From c9ad2e14515537c1f7f3857804f69bb7fb05b3b1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 8 Jul 2020 13:55:28 -0700 Subject: [PATCH 01/68] 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 36d73c7ab..e9cf73946 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 ff0dc5e64..5e7a05862 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -21,13 +21,13 @@ def _locked( ) -> 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 @@ def _locked( # "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 02/68] 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 77734ee64..91c430555 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 03/68] 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 f676fb192..5f17d9c12 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 874eb53a5..ffcc2e875 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 d14a171f6..4e131dff7 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 c4724768c..f7abeeb4d 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 04/68] 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 b2321ae0d..13d78cbbd 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 833bb8f83..d066e5728 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 05/68] 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 56ec0dd1b..8dfa9473e 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -12,8 +12,10 @@ 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 @@ class InvalidManifestError(FatalError): 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 @@ def ordered_load_normalize_legacy_config(contents: str) -> Dict[str, Any]: 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 eb906b78f..4ddfdf5b3 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 ffcc2e875..86479607c 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -8,8 +8,8 @@ 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 @@ 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 c48adbde9..2e2f738c9 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 06/68] 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 567b7cd3b..1f28c8c74 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 07/68] 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 6d8c40a93..809a6f4dc 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 @@ def _create_config_table(self, db: sqlite3.Connection) -> None: ) 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 6a4e900c9..0947144ed 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 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 08/68] 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 c487acf6d..e692f3dce 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 0ce58b1a2..c9d7f82e7 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 09/68] 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 4091492cc..9c1311988 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -7,9 +7,7 @@ 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 0c51df628..311d1277d 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -3,7 +3,6 @@ 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 4edb7a9ec..f556a8dd9 100644 --- a/testing/util.py +++ b/testing/util.py @@ -5,14 +5,24 @@ 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 b65b2235a..3bed4bfa5 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 10/68] 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 6f7c90055..7a685808e 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 c419ad621..29c5a9bf2 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -8,6 +8,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 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 11/68] 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 c8b7633b6..85fa53cbb 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 91c430555..46e96c1dc 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 809a6f4dc..e5522ec33 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -79,7 +79,7 @@ def __init__(self, directory: Optional[str] = None) -> None: 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 12/68] 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 e692f3dce..a92a6b36c 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 c9d7f82e7..4153d7650 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 13/68] 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 d99e6f2c7..dccbb7ca2 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -21,7 +21,6 @@ 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 fd300469a..c8e2d47d1 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 14/68] 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 13d78cbbd..009f6d9c2 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 d6a13dc43..14ada96ed 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 5809a3f27..481a7279d 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -3,6 +3,8 @@ 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 @@ def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): ) -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 d3ec3fda2..a157d1636 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -3,6 +3,8 @@ 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 d066e5728..5dc085059 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 84e4da930..035b02a65 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 @@ 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 15/68] 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 85fa53cbb..684b59805 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 481a7279d..7a4b90635 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 16/68] 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 7a685808e..afa093d56 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 17/68] 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 73b23cc07..ef73961f1 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 18/68] 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 8dfa9473e..87679bfa6 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -13,7 +13,7 @@ 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 009f6d9c2..afacab9bb 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -7,14 +7,11 @@ 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 000000000..f84d3f185 --- /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 576bef8cc..ca30eaa7e 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -6,6 +6,8 @@ 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 86479607c..c1eb104ac 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -23,10 +23,8 @@ 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 5dc085059..804701f05 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -7,6 +7,7 @@ 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 f7abeeb4d..6738df683 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -6,7 +6,7 @@ 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 19/68] 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 a92a6b36c..1621bb3fc 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 071757a1f..d634e4931 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 1fc920725..4f6136ede 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 20/68] 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 c31bcd714..d320b830d 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -15,8 +15,8 @@ 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 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 z 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 M+Dxp-i343@S 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 literal 31781 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%0DX^HE0 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%0AcDMh%UGT>#`||~+1FOtBlZ%SrXoNM@14%S& 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 BOpgEn From 58a190fd69d43c420195b0de5f16345115d1a071 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 23 Sep 2020 17:44:18 -0700 Subject: [PATCH 21/68] 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 afa093d56..65f521cdc 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 29c5a9bf2..cfe14834f 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 22/68] 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 d435faa8c..64617c333 100644 --- a/pre_commit/commands/sample_config.py +++ b/pre_commit/commands/sample_config.py @@ -7,7 +7,7 @@ # 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 11c087649..8e3a9043f 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 23/68] 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 5428b0ad8..4f4f6b941 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 5609631b0..f32780c14 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -8,6 +8,7 @@ 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 @@ class Language(NamedTuple): '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 000000000..a8abc8611 --- /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 2bff7beb0..35eac042b 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 000000000..edcd28f4a --- /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 000000000..d005a74cc --- /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 000000000..1456e8ef2 --- /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 000000000..d2e556ac0 --- /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 000000000..edcd28f4a --- /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 000000000..d005a74cc --- /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 000000000..04ad4e0cc --- /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 000000000..e37296480 --- /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 000000000..87d2afbaf --- /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 000000000..e69de29bb diff --git a/tests/repository_test.py b/tests/repository_test.py index 035b02a65..3f7a39fbf 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 63a3aab81..11b20d418 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 24/68] 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 000000000..e21d5fe31 --- /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 000000000..73a984d49 --- /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 000000000..e437d1df5 --- /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 000000000..752768de6 --- /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 25/68] 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 14ada96ed..56afd41f5 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 73a984d49..f0a345e6a 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 752768de6..a644946d5 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 e437d1df5..97c5928e3 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 26/68] 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 3f7a39fbf..a6d801ec1 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 27/68] 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 9c1311988..9d30568c5 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 28/68] 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 cabea22ec..6eef56b7a 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 29/68] 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 40adba0f7..c80d6794b 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 6eef56b7a..d8bacc484 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 30/68] 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 98a6d00e0..de7032cb9 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 fb400107d..41f1e5f9b 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 31/68] 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 41f1e5f9b..e7256da18 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 f32780c14..9c2e59d78 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -6,6 +6,7 @@ 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 000000000..2841467fc --- /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 35eac042b..d9b01bd04 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 000000000..42e563549 --- /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 000000000..760c6c125 --- /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 000000000..37f401e2c --- /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 000000000..d4a143b3d --- /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 f556a8dd9..18cd73427 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 a6d801ec1..3d5093df6 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -31,6 +31,7 @@ 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 32/68] 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 afacab9bb..023dd3596 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -12,7 +12,12 @@ 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 804701f05..6b0bb86d7 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 33/68] 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 1f28c8c74..0d335e285 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -11,6 +11,7 @@ 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.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 @@ def from_config( 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 16d3d15e3..4ab0d8cb9 100644 --- a/pre_commit/envcontext.py +++ b/pre_commit/envcontext.py @@ -2,13 +2,12 @@ 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 @@ class Var(NamedTuple): 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 ca30eaa7e..13ba664c8 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -3,6 +3,7 @@ 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.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 0338b3737..f4cf7045a 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -16,7 +16,6 @@ from typing import Optional from typing import Tuple from typing import Type -from typing import Union import yaml @@ -29,8 +28,6 @@ from importlib_resources import open_binary from importlib_resources import read_text -EnvironT = Union[Dict[str, str], 'os._Environ'] - Loader = getattr(yaml, 'CSafeLoader', yaml.SafeLoader) yaml_load = functools.partial(yaml.load, Loader=Loader) Dumper = getattr(yaml, 'CSafeDumper', yaml.SafeDumper) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 5235dc650..7538b54f6 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -9,6 +9,7 @@ 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 @@ -17,13 +18,12 @@ 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: Optional[EnvironT] = None) -> 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 2461ed5b3..00b471282 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -2,6 +2,7 @@ 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 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 34/68] 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 dccbb7ca2..59e534068 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -19,6 +19,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 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 c8e2d47d1..8e52268ff 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 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 35/68] 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 01c65ab69..69e127878 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 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 @@ 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 59e534068..8dc4e8ba9 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -7,7 +7,6 @@ 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 ef73961f1..b6c0bd79f 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -8,7 +8,6 @@ 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 @@ @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 fa493cc04..2e8277e02 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 36/68] 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 e9cf73946..80fa14bbe 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 1621bb3fc..a56701e3b 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 4153d7650..eb7a8e199 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 37/68] show features first --- CHANGELOG.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a56701e3b..0ae2d7452 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 38/68] 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 69e127878..29138fd1a 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 2e8277e02..669cd3343 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 39/68] 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 0ae2d7452..c26eb8af8 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 eb7a8e199..94f14ad45 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 40/68] 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 b6c0bd79f..1a0f0c7e3 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 853bb7321..6c0c9e5e0 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_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_rbenv(tempdir_factory): - prefix = Prefix(tempdir_factory.get()) - ruby._install_rbenv(prefix, C.DEFAULT) +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 41/68] 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 c26eb8af8..ff1013f82 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 94f14ad45..32160b9ea 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 42/68] 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 87679bfa6..0b8582bce 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -61,6 +61,7 @@ def _make_argparser(filenames_help: str) -> argparse.ArgumentParser: 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 0d335e285..56450e384 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -83,20 +83,28 @@ def by_types( 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 b65ac42b0..ea773942b 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 db6865c6c..12be03f8a 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 bdade5132..a828db4d2 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 229ccaf41..7dcffebe8 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 5af177a83..a9f1dcd91 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 6565ee40a..cbc4b3544 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 000000000..a4ea920d6 --- /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 000000000..a828db4d2 --- /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 bdade5132..a828db4d2 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 00b471282..34f3a3753 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 3d5093df6..8d771458f 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 43/68] 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 a8abc8611..094d2f1ce 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -12,7 +12,6 @@ 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 44/68] 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 d580ff178..621c7e9ad 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.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 6a049d5f6..f5c89d044 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -2,24 +2,9 @@ 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 45/68] only perform migrate_config parsing if it is a list --- pre_commit/commands/migrate_config.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index d580ff178..1055c9f38 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 + 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:]) + header = ''.join(lines[:i]) + rest = ''.join(lines[i:]) - if isinstance(yaml_load(contents), list): # 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 46/68] 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 7538b54f6..60a057c19 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 4f6136ede..7e83ef590 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 47/68] 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 87f6d53d2..7320bb426 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 48/68] 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 bd89c1dba..b2bad6014 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 49/68] 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 4ab0d8cb9..92d975d09 100644 --- a/pre_commit/envcontext.py +++ b/pre_commit/envcontext.py @@ -8,11 +8,7 @@ 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 50/68] [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 80fa14bbe..73692993b 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 51/68] 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 1a0f0c7e3..81bc95436 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 52/68] 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 000000000..9408e44d6 --- /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 53/68] 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 56450e384..508b61a34 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 13ba664c8..8e22dcf0f 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 54/68] 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 73692993b..72ce7bf42 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 ff1013f82..4c7032b59 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 32160b9ea..9b15fe1e6 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 55/68] 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 7320bb426..33a347302 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 56/68] 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 0b8582bce..d619ea523 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -112,6 +112,18 @@ def validate_manifest_main(argv: Optional[Sequence[str]] = None) -> int: 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 @@ def warn_unknown_keys_repo( 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 2e2f738c9..bfb754b66 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 57/68] 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 65f521cdc..43b728082 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 cfe14834f..90d1036a3 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 58/68] 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 72ce7bf42..1b993e8c5 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 4c7032b59..9489b15d5 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 9b15fe1e6..9188df1b6 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 59/68] 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 a644946d5..8740b2f5a 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 60/68] 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 d619ea523..20d44925a 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -61,7 +61,7 @@ def _make_argparser(filenames_help: str) -> argparse.ArgumentParser: 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 508b61a34..1e8fad23b 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -92,7 +92,11 @@ def by_types( 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 34f3a3753..b4491d01f 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 8d771458f..d513cb71f 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 61/68] 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 1b993e8c5..f768a5b7e 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 9489b15d5..d3773f6ed 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 9188df1b6..ed87cb1ac 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 62/68] 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 b4491d01f..914d567a9 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 63/68] 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 d3773f6ed..e833f9f3d 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 64/68] 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 8e22dcf0f..156e53d20 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 65/68] 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 f4cf7045a..fc506b98e 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -255,7 +255,7 @@ def handle_remove_readonly( 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 66/68] 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 fc506b98e..b5f40ada4 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -255,7 +255,7 @@ def handle_remove_readonly( 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 67/68] 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 156e53d20..50962745b 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 68/68] 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 f768a5b7e..d42bb1b11 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 e833f9f3d..ef36decce 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 ed87cb1ac..2e77fcf4e 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