From 44b2af80f4c29dd2ac52d25cbe2a82be3a695a73 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 2 Jul 2017 21:25:49 -0700 Subject: [PATCH 001/544] dogfood: upgrade hooks --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 31ea2398a..f8987870e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ - repo: https://github.com/pre-commit/pre-commit-hooks.git - sha: v0.7.0 + sha: v0.9.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -12,12 +12,12 @@ - id: requirements-txt-fixer - id: flake8 - repo: https://github.com/pre-commit/pre-commit.git - sha: v0.12.0 + sha: v0.15.0 hooks: - id: validate_config - id: validate_manifest - repo: https://github.com/asottile/reorder_python_imports.git - sha: v0.3.1 + sha: v0.3.4 hooks: - id: reorder-python-imports language_version: python2.7 From 096f906912cba316ea890e089599f2ad6a9af277 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 2 Jul 2017 21:49:41 -0700 Subject: [PATCH 002/544] More specific symlink testing without checking in a symlink --- testing/test_symlink | 1 - tests/git_test.py | 13 ++++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) delete mode 120000 testing/test_symlink diff --git a/testing/test_symlink b/testing/test_symlink deleted file mode 120000 index ee1f6cb7f..000000000 --- a/testing/test_symlink +++ /dev/null @@ -1 +0,0 @@ -does_not_exist \ No newline at end of file diff --git a/tests/git_test.py b/tests/git_test.py index ffe1c1aad..4ffccee3a 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -10,6 +10,7 @@ from pre_commit.util import cmd_output from pre_commit.util import cwd from testing.fixtures import git_dir +from testing.util import xfailif_no_symlink def test_get_root_at_root(tempdir_factory): @@ -72,7 +73,6 @@ def get_filenames(): 'pre_commit/main.py', 'pre_commit/git.py', 'im_a_file_that_doesnt_exist.py', - 'testing/test_symlink', ) return git.get_files_matching(get_filenames) @@ -84,10 +84,17 @@ def test_get_files_matching_base(get_files_matching_func): '.pre-commit-hooks.yaml', 'pre_commit/main.py', 'pre_commit/git.py', - 'testing/test_symlink' } +@xfailif_no_symlink +def test_matches_broken_symlink(tmpdir): # pragma: no cover (non-windwos) + with tmpdir.as_cwd(): + os.symlink('does-not-exist', 'link') + func = git.get_files_matching(lambda: ('link',)) + assert func('', '^$') == {'link'} + + def test_get_files_matching_total_match(get_files_matching_func): ret = get_files_matching_func('^.*\\.py$', '^$') assert ret == {'pre_commit/main.py', 'pre_commit/git.py'} @@ -105,7 +112,7 @@ def test_does_not_include_deleted_fileS(get_files_matching_func): def test_exclude_removes_files(get_files_matching_func): ret = get_files_matching_func('', '\\.py$') - assert ret == {'.pre-commit-hooks.yaml', 'testing/test_symlink'} + assert ret == {'.pre-commit-hooks.yaml'} def resolve_conflict(): From a4da7b8c8c03f5be9159360365626c87a196d5bc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 8 Jul 2017 15:43:36 -0700 Subject: [PATCH 003/544] Replace calls to touch with open(..., 'a').close() --- tests/commands/install_uninstall_test.py | 2 +- tests/conftest.py | 2 +- tests/make_archives_test.py | 4 ++-- tests/store_test.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index ad8d2456d..d00d55d77 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -118,7 +118,7 @@ def test_uninstall(tempdir_factory): def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): - cmd_output('touch', touch_file) + open(touch_file, 'a').close() cmd_output('git', 'add', touch_file) return cmd_output_mocked_pre_commit_home( 'git', 'commit', '-am', 'Commit!', '--allow-empty', diff --git a/tests/conftest.py b/tests/conftest.py index 140463b15..9813e9aca 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -68,7 +68,7 @@ def _make_conflict(): def in_merge_conflict(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - cmd_output('touch', 'dummy') + open('dummy', 'a').close() cmd_output('git', 'add', 'dummy') cmd_output('git', 'commit', '-m', 'Add config.') diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py index f3636b535..5aa303f77 100644 --- a/tests/make_archives_test.py +++ b/tests/make_archives_test.py @@ -20,13 +20,13 @@ def test_make_archive(tempdir_factory): git_path = git_dir(tempdir_factory) # Add a files to the git directory with cwd(git_path): - cmd_output('touch', 'foo') + open('foo', 'a').close() cmd_output('git', 'add', '.') cmd_output('git', 'commit', '-m', 'foo') # We'll use this sha head_sha = get_head_sha('.') # And check that this file doesn't exist - cmd_output('touch', 'bar') + open('bar', 'a').close() cmd_output('git', 'add', '.') cmd_output('git', 'commit', '-m', 'bar') diff --git a/tests/store_test.py b/tests/store_test.py index 268579655..1bbcf44a2 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -46,7 +46,7 @@ def test_store_require_created(store): # Should create the store directory assert os.path.exists(store.directory) # Should create a README file indicating what the directory is about - with io.open(os.path.join(store.directory, 'README'), 'r') as readme_file: + with io.open(os.path.join(store.directory, 'README')) as readme_file: readme_contents = readme_file.read() for text_line in ( 'This directory is maintained by the pre-commit project.', @@ -73,7 +73,7 @@ def test_does_not_recreate_if_directory_already_exists(store): # Note: we're intentionally leaving out the README file. This is so we can # know that `Store` didn't call create os.mkdir(store.directory) - io.open(store.db_path, 'a+').close() + open(store.db_path, 'a').close() # Call require_created, this should not call create store.require_created() assert not os.path.exists(os.path.join(store.directory, 'README')) From d876661345d4917e59beeff699a2a8a4fdb88d8b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 8 Jul 2017 20:22:36 -0700 Subject: [PATCH 004/544] Use a more intelligent default language version --- pre_commit/languages/all.py | 14 +++++---- pre_commit/languages/docker.py | 5 ++- pre_commit/languages/golang.py | 7 ++--- pre_commit/languages/helpers.py | 4 +++ pre_commit/languages/node.py | 5 ++- pre_commit/languages/pcre.py | 8 ++--- pre_commit/languages/python.py | 54 ++++++++++++++++++++++++++++----- pre_commit/languages/ruby.py | 5 ++- pre_commit/languages/script.py | 7 ++--- pre_commit/languages/swift.py | 5 ++- pre_commit/languages/system.py | 7 ++--- pre_commit/manifest.py | 9 +++++- tests/languages/all_test.py | 13 ++++++-- tests/manifest_test.py | 13 ++++++-- tests/repository_test.py | 7 +++-- 15 files changed, 109 insertions(+), 54 deletions(-) diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index f441ddd27..5546025d8 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -10,16 +10,18 @@ from pre_commit.languages import swift from pre_commit.languages import system -# A language implements the following constant and two functions in its module: +# A language implements the following constant and functions in its module: # # # Use None for no environment # ENVIRONMENT_DIR = 'foo_env' # -# def install_environment( -# repo_cmd_runner, -# version='default', -# additional_dependencies=(), -# ): +# def get_default_version(): +# """Return a value to replace the 'default' value for language_version. +# +# return 'default' if there is no better option. +# """ +# +# def install_environment(repo_cmd_runner, version, additional_dependencies): # """Installs a repository in the given repository. Note that the current # working directory will already be inside the repository. # diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 7d3f8d041..59dc1b414 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -14,6 +14,7 @@ ENVIRONMENT_DIR = 'docker' PRE_COMMIT_LABEL = 'PRE_COMMIT' +get_default_version = helpers.basic_get_default_version def md5(s): # pragma: windows no cover @@ -55,9 +56,7 @@ def build_docker_image(repo_cmd_runner, **kwargs): # pragma: windows no cover def install_environment( - repo_cmd_runner, - version='default', - additional_dependencies=(), + repo_cmd_runner, version, additional_dependencies, ): # pragma: windows no cover assert repo_cmd_runner.exists('Dockerfile'), ( 'No Dockerfile was found in the hook repository' diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index c0bfbcbcb..ee04ca796 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -14,6 +14,7 @@ ENVIRONMENT_DIR = 'golangenv' +get_default_version = helpers.basic_get_default_version def get_env_patch(venv): @@ -44,11 +45,7 @@ def guess_go_dir(remote_url): return 'unknown_src_dir' -def install_environment( - repo_cmd_runner, - version='default', - additional_dependencies=(), -): +def install_environment(repo_cmd_runner, version, additional_dependencies): helpers.assert_version_default('golang', version) directory = repo_cmd_runner.path( helpers.environment_dir(ENVIRONMENT_DIR, 'default'), diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index a6c93de18..6af77e307 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -33,3 +33,7 @@ def assert_no_additional_deps(lang, additional_deps): 'For now, pre-commit does not support ' 'additional_dependencies for {}'.format(lang), ) + + +def basic_get_default_version(): + return 'default' diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index ef557a16b..b5f7c56e0 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -12,6 +12,7 @@ ENVIRONMENT_DIR = 'node_env' +get_default_version = helpers.basic_get_default_version def get_env_patch(venv): # pragma: windows no cover @@ -34,9 +35,7 @@ def in_env(repo_cmd_runner, language_version): # pragma: windows no cover def install_environment( - repo_cmd_runner, - version='default', - additional_dependencies=(), + repo_cmd_runner, version, additional_dependencies, ): # pragma: windows no cover additional_dependencies = tuple(additional_dependencies) assert repo_cmd_runner.exists('package.json') diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py index 314ea090d..faba53957 100644 --- a/pre_commit/languages/pcre.py +++ b/pre_commit/languages/pcre.py @@ -2,18 +2,16 @@ import sys +from pre_commit.languages import helpers from pre_commit.xargs import xargs ENVIRONMENT_DIR = None GREP = 'ggrep' if sys.platform == 'darwin' else 'grep' +get_default_version = helpers.basic_get_default_version -def install_environment( - repo_cmd_runner, - version='default', - additional_dependencies=(), -): +def install_environment(repo_cmd_runner, version, additional_dependencies): """Installation for pcre type is a noop.""" raise AssertionError('Cannot install pcre repo.') diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 634abe587..715d585ff 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals import contextlib -import distutils.spawn import os import sys @@ -9,11 +8,13 @@ from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var from pre_commit.languages import helpers +from pre_commit.parse_shebang import find_executable from pre_commit.util import clean_path_on_failure from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'py_env' +get_default_version = helpers.basic_get_default_version def bin_dir(venv): @@ -39,10 +40,53 @@ def in_env(repo_cmd_runner, language_version): yield +def _get_default_version(): # pragma: no cover (platform dependent) + def _norm(path): + _, exe = os.path.split(path.lower()) + exe, _, _ = exe.partition('.exe') + if find_executable(exe) and exe not in {'python', 'pythonw'}: + return exe + + # First attempt from `sys.executable` (or the realpath) + # On linux, I see these common sys.executables: + # + # system `python`: /usr/bin/python -> python2.7 + # system `python2`: /usr/bin/python2 -> python2.7 + # virtualenv v: v/bin/python (will not return from this loop) + # virtualenv v -ppython2: v/bin/python -> python2 + # virtualenv v -ppython2.7: v/bin/python -> python2.7 + # virtualenv v -ppypy: v/bin/python -> v/bin/pypy + for path in {sys.executable, os.path.realpath(sys.executable)}: + exe = _norm(path) + if exe: + return exe + + # Next try the `pythonX.X` executable + exe = 'python{}.{}'.format(*sys.version_info) + if find_executable(exe): + return exe + + # Give a best-effort try for windows + if os.path.exists(r'C:\{}\python.exe'.format(exe.replace('.', ''))): + return exe + + # We tried! + return 'default' + + +def get_default_version(): + # TODO: when dropping python2, use `functools.lru_cache(maxsize=1)` + try: + return get_default_version.cached_version + except AttributeError: + get_default_version.cached_version = _get_default_version() + return get_default_version() + + def norm_version(version): if os.name == 'nt': # pragma: no cover (windows) # Try looking up by name - if distutils.spawn.find_executable(version): + if find_executable(version) and find_executable(version) != version: return version # If it is in the form pythonx.x search in the default @@ -54,11 +98,7 @@ def norm_version(version): return os.path.expanduser(version) -def install_environment( - repo_cmd_runner, - version='default', - additional_dependencies=(), -): +def install_environment(repo_cmd_runner, version, additional_dependencies): additional_dependencies = tuple(additional_dependencies) directory = helpers.environment_dir(ENVIRONMENT_DIR, version) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index d3896d904..26e303c3a 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -16,6 +16,7 @@ ENVIRONMENT_DIR = 'rbenv' +get_default_version = helpers.basic_get_default_version def get_env_patch(venv, language_version): # pragma: windows no cover @@ -97,9 +98,7 @@ def _install_ruby(runner, version): # pragma: windows no cover def install_environment( - repo_cmd_runner, - version='default', - additional_dependencies=(), + repo_cmd_runner, version, additional_dependencies, ): # pragma: windows no cover additional_dependencies = tuple(additional_dependencies) directory = helpers.environment_dir(ENVIRONMENT_DIR, version) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 762ae7634..c4b6593d5 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -5,13 +5,10 @@ ENVIRONMENT_DIR = None +get_default_version = helpers.basic_get_default_version -def install_environment( - repo_cmd_runner, - version='default', - additional_dependencies=(), -): +def install_environment(repo_cmd_runner, version, additional_dependencies): """Installation for script type is a noop.""" raise AssertionError('Cannot install script repo.') diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 4d171c5be..a27dfac24 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -10,6 +10,7 @@ from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'swift_env' +get_default_version = helpers.basic_get_default_version BUILD_DIR = '.build' BUILD_CONFIG = 'release' @@ -29,9 +30,7 @@ def in_env(repo_cmd_runner): # pragma: windows no cover def install_environment( - repo_cmd_runner, - version='default', - additional_dependencies=(), + repo_cmd_runner, version, additional_dependencies, ): # pragma: windows no cover helpers.assert_version_default('swift', version) helpers.assert_no_additional_deps('swift', additional_dependencies) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index c9e1c5dca..31480792c 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -5,13 +5,10 @@ ENVIRONMENT_DIR = None +get_default_version = helpers.basic_get_default_version -def install_environment( - repo_cmd_runner, - version='default', - additional_dependencies=(), -): +def install_environment(repo_cmd_runner, version, additional_dependencies): """Installation for system type is a noop.""" raise AssertionError('Cannot install system repo.') diff --git a/pre_commit/manifest.py b/pre_commit/manifest.py index 888ad6dda..081f3c603 100644 --- a/pre_commit/manifest.py +++ b/pre_commit/manifest.py @@ -7,6 +7,7 @@ import pre_commit.constants as C from pre_commit.clientlib import load_manifest +from pre_commit.languages.all import languages logger = logging.getLogger('pre_commit') @@ -38,4 +39,10 @@ def manifest_contents(self): @cached_property def hooks(self): - return {hook['id']: hook for hook in self.manifest_contents} + ret = {} + for hook in self.manifest_contents: + if hook['language_version'] == 'default': + language = languages[hook['language']] + hook['language_version'] = language.get_default_version() + ret[hook['id']] = hook + return ret diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index 73b89cb5d..dd1ed27b2 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -12,9 +12,7 @@ def test_install_environment_argspec(language): expected_argspec = inspect.ArgSpec( args=['repo_cmd_runner', 'version', 'additional_dependencies'], - varargs=None, - keywords=None, - defaults=('default', ()), + varargs=None, keywords=None, defaults=None, ) argspec = inspect.getargspec(languages[language].install_environment) assert argspec == expected_argspec @@ -33,3 +31,12 @@ def test_run_hook_argpsec(language): ) argspec = inspect.getargspec(languages[language].run_hook) assert argspec == expected_argspec + + +@pytest.mark.parametrize('language', all_languages) +def test_get_default_version_argspec(language): + expected_argspec = inspect.ArgSpec( + args=[], varargs=None, keywords=None, defaults=None, + ) + argspec = inspect.getargspec(languages[language].get_default_version) + assert argspec == expected_argspec diff --git a/tests/manifest_test.py b/tests/manifest_test.py index 7db886c50..ada004fcc 100644 --- a/tests/manifest_test.py +++ b/tests/manifest_test.py @@ -11,8 +11,7 @@ @pytest.yield_fixture def manifest(store, tempdir_factory): path = make_repo(tempdir_factory, 'script_hooks_repo') - head_sha = get_head_sha(path) - repo_path = store.clone(path, head_sha) + repo_path = store.clone(path, get_head_sha(path)) yield Manifest(repo_path, path) @@ -76,3 +75,13 @@ def test_legacy_manifest_warn(store, tempdir_factory, log_warning_mock): 'If `pre-commit autoupdate` does not silence this warning consider ' 'making an issue / pull request.'.format(path) ) + + +def test_default_python_language_version(store, tempdir_factory): + path = make_repo(tempdir_factory, 'python_hooks_repo') + repo_path = store.clone(path, get_head_sha(path)) + manifest = Manifest(repo_path, path) + + # This assertion is difficult as it is version dependent, just assert + # that it is *something* + assert manifest.hooks['foo']['language_version'] != 'default' diff --git a/tests/repository_test.py b/tests/repository_test.py index f91642ee4..7131d75b9 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -442,7 +442,7 @@ def test_venvs(tempdir_factory, store): config = make_config_from_repo(path) repo = Repository.create(config, store) venv, = repo._venvs - assert venv == (mock.ANY, 'python', 'default', []) + assert venv == (mock.ANY, 'python', python.get_default_version(), []) @pytest.mark.integration @@ -452,7 +452,7 @@ def test_additional_dependencies(tempdir_factory, store): config['hooks'][0]['additional_dependencies'] = ['pep8'] repo = Repository.create(config, store) venv, = repo._venvs - assert venv == (mock.ANY, 'python', 'default', ['pep8']) + assert venv == (mock.ANY, 'python', python.get_default_version(), ['pep8']) @pytest.mark.integration @@ -591,7 +591,8 @@ class MyKeyboardInterrupt(KeyboardInterrupt): repo.run_hook(hook, []) # Should have made an environment, however this environment is broken! - assert os.path.exists(repo._cmd_runner.path('py_env-default')) + envdir = 'py_env-{}'.format(python.get_default_version()) + assert repo._cmd_runner.exists(envdir) # However, it should be perfectly runnable (reinstall after botched # install) From e2bae300fe2794e2ece25d4cd72127238704bb1e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 9 Jul 2017 10:22:19 -0700 Subject: [PATCH 005/544] v0.15.1 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d929f8942..fc3cf4db9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.15.1 +====== +- Use a more intelligent default language version for python + 0.15.0 ====== - Add `types` and `exclude_types` for filtering files. These options take diff --git a/setup.py b/setup.py index 3c94bfa32..c2bd490d0 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.15.0', + version='0.15.1', author='Anthony Sottile', author_email='asottile@umich.edu', From 4262487d12ea59669ebcbb8ff75d344dbefa8635 Mon Sep 17 00:00:00 2001 From: Evan Hubinger Date: Sun, 9 Jul 2017 13:40:51 -0700 Subject: [PATCH 006/544] Fix windows virtualenv issue --- pre_commit/languages/python.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 715d585ff..eea156b40 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -86,8 +86,9 @@ def get_default_version(): def norm_version(version): if os.name == 'nt': # pragma: no cover (windows) # Try looking up by name - if find_executable(version) and find_executable(version) != version: - return version + version_exec = find_executable(version) + if version_exec and version_exec != version: + return version_exec # If it is in the form pythonx.x search in the default # place on windows From 33a3ceb1297dea34c424d8e6faf3c6f2a54ef894 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 9 Jul 2017 14:11:17 -0700 Subject: [PATCH 007/544] v0.15.2 --- CHANGELOG.md | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc3cf4db9..513671666 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +0.15.2 +====== +- Work around a windows-specific virtualenv bug pypa/virtualenv#1062 + This failure mode was introduced in 0.15.1 + 0.15.1 ====== - Use a more intelligent default language version for python diff --git a/setup.py b/setup.py index c2bd490d0..424bfd6f4 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.15.1', + version='0.15.2', author='Anthony Sottile', author_email='asottile@umich.edu', From fb7d6c7b0cfc7d8f129a326c0e984043a2ee4229 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 9 Jul 2017 14:40:30 -0700 Subject: [PATCH 008/544] Remove pypy3 workarounds since we don't test pypy3.2 --- requirements-dev.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e18122267..157f287d3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,6 +5,3 @@ flake8 mock pytest pytest-env - -# setuptools breaks pypy3 with extraneous output -setuptools<18.5 From 0c70fa422929c43e5c34d7407bfd65c08bbcc365 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 12 Jul 2017 18:30:51 -0700 Subject: [PATCH 009/544] Use asottile/add-trailing-comma --- .pre-commit-config.yaml | 4 ++++ pre_commit/color_windows.py | 6 +++--- pre_commit/commands/autoupdate.py | 2 +- pre_commit/commands/install_uninstall.py | 2 +- pre_commit/commands/run.py | 2 +- pre_commit/git.py | 6 +++--- pre_commit/languages/docker.py | 4 ++-- pre_commit/languages/python.py | 2 +- pre_commit/languages/ruby.py | 2 +- pre_commit/logging_handler.py | 2 +- pre_commit/main.py | 6 +++--- pre_commit/make_archives.py | 2 +- pre_commit/manifest.py | 2 +- pre_commit/output.py | 2 +- pre_commit/prefixed_command_runner.py | 2 +- pre_commit/repository.py | 6 +++--- pre_commit/schema.py | 2 +- pre_commit/staged_files_only.py | 2 +- pre_commit/store.py | 4 ++-- pre_commit/util.py | 6 +++--- testing/fixtures.py | 2 +- testing/util.py | 4 ++-- tests/clientlib_test.py | 2 +- tests/commands/autoupdate_test.py | 8 ++++---- tests/commands/install_uninstall_test.py | 10 +++++----- tests/commands/run_test.py | 24 ++++++++++++------------ tests/languages/docker_test.py | 2 +- tests/prefixed_command_runner_test.py | 2 +- tests/repository_test.py | 12 ++++++------ tests/runner_test.py | 4 ++-- tests/schema_test.py | 2 +- tests/store_test.py | 6 +++--- 32 files changed, 74 insertions(+), 70 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f8987870e..46a47341a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,3 +21,7 @@ hooks: - id: reorder-python-imports language_version: python2.7 +- repo: https://github.com/asottile/add-trailing-comma + sha: v0.3.0 + hooks: + - id: add-trailing-comma diff --git a/pre_commit/color_windows.py b/pre_commit/color_windows.py index d44e0b809..dae41afe8 100644 --- a/pre_commit/color_windows.py +++ b/pre_commit/color_windows.py @@ -20,18 +20,18 @@ def bool_errcheck(result, func, args): GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)( ("GetStdHandle", windll.kernel32), - ((1, "nStdHandle"), ) + ((1, "nStdHandle"), ), ) GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))( ("GetConsoleMode", windll.kernel32), - ((1, "hConsoleHandle"), (2, "lpMode")) + ((1, "hConsoleHandle"), (2, "lpMode")), ) GetConsoleMode.errcheck = bool_errcheck SetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, DWORD)( ("SetConsoleMode", windll.kernel32), - ((1, "hConsoleHandle"), (1, "dwMode")) + ((1, "hConsoleHandle"), (1, "dwMode")), ) SetConsoleMode.errcheck = bool_errcheck diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 99a5d62fd..620a8a6e6 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -59,7 +59,7 @@ def _update_repo(repo_config, runner, tags_only): if hooks_missing: raise RepositoryCannotBeUpdatedError( 'Cannot update because the tip of master is missing these hooks:\n' - '{}'.format(', '.join(sorted(hooks_missing))) + '{}'.format(', '.join(sorted(hooks_missing))), ) return new_config diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 6268b918f..36b0d7d79 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -51,7 +51,7 @@ def install( 'Running in migration mode with existing hooks at {}\n' 'Use -f to use only pre-commit.'.format( legacy_path, - ) + ), ) with io.open(hook_path, 'w') as pre_commit_file_obj: diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 99d3a189e..676c50442 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -235,7 +235,7 @@ def run(runner, args, environ=os.environ): logger.error( 'Your .pre-commit-config.yaml is unstaged.\n' '`git add .pre-commit-config.yaml` to fix this.\n' - 'Run pre-commit with --allow-unstaged-config to silence this.' + 'Run pre-commit with --allow-unstaged-config to silence this.', ) return 1 diff --git a/pre_commit/git.py b/pre_commit/git.py index 754514aa8..4b519c86a 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -21,7 +21,7 @@ def get_root(): except CalledProcessError: raise FatalError( 'git failed. Is it installed, and are you in a Git repository ' - 'directory?' + 'directory?', ) @@ -79,7 +79,7 @@ def get_staged_files(): return cmd_output( 'git', 'diff', '--staged', '--name-only', '--no-ext-diff', # Everything except for D - '--diff-filter=ACMRTUXB' + '--diff-filter=ACMRTUXB', )[1].splitlines() @@ -130,5 +130,5 @@ def check_for_cygwin_mismatch(): ' - git {}\n'.format( exe_type[is_cygwin_python], exe_type[is_cygwin_git], - ) + ), ) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 59dc1b414..8404ac84c 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -23,7 +23,7 @@ def md5(s): # pragma: windows no cover def docker_tag(repo_cmd_runner): # pragma: windows no cover return 'pre-commit-{}'.format( - md5(os.path.basename(repo_cmd_runner.path())) + md5(os.path.basename(repo_cmd_runner.path())), ).lower() @@ -92,7 +92,7 @@ def run_hook(repo_cmd_runner, hook, file_args): # pragma: windows no cover '-v', '{}:/src:rw'.format(os.getcwd()), '--workdir', '/src', '--entrypoint', entry_executable, - docker_tag(repo_cmd_runner) + docker_tag(repo_cmd_runner), ) + cmd_rest return xargs(cmd, file_args) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index eea156b40..11f377654 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -107,7 +107,7 @@ def install_environment(repo_cmd_runner, version, additional_dependencies): with clean_path_on_failure(repo_cmd_runner.path(directory)): venv_cmd = [ sys.executable, '-m', 'virtualenv', - '{{prefix}}{}'.format(directory) + '{{prefix}}{}'.format(directory), ] if version != 'default': venv_cmd.extend(['-p', norm_version(version)]) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 26e303c3a..6a0bde277 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -81,7 +81,7 @@ def _install_rbenv( # directory "export GEM_HOME='{directory}/gems'\n" 'export PATH="$GEM_HOME/bin:$PATH"\n' - '\n'.format(directory=repo_cmd_runner.path(directory)) + '\n'.format(directory=repo_cmd_runner.path(directory)), ) # If we aren't using the system ruby, add a version here diff --git a/pre_commit/logging_handler.py b/pre_commit/logging_handler.py index 78c2827af..7241cd679 100644 --- a/pre_commit/logging_handler.py +++ b/pre_commit/logging_handler.py @@ -30,7 +30,7 @@ def emit(self, record): self.use_color, ) + ' ', record.getMessage(), - ) + ), ) diff --git a/pre_commit/main.py b/pre_commit/main.py index baaf84b69..02eed40e0 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -41,7 +41,7 @@ def _add_color_option(parser): def _add_config_option(parser): parser.add_argument( '-c', '--config', default='.pre-commit-config.yaml', - help='Path to alternate config file' + help='Path to alternate config file', ) @@ -228,11 +228,11 @@ def main(argv=None): return sample_config() else: raise NotImplementedError( - 'Command {} not implemented.'.format(args.command) + 'Command {} not implemented.'.format(args.command), ) raise AssertionError( - 'Command {} failed to exit with a returncode'.format(args.command) + 'Command {} failed to exit with a returncode'.format(args.command), ) diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index 4baaaa18f..c672fc181 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -28,7 +28,7 @@ RESOURCES_DIR = os.path.abspath( - os.path.join(os.path.dirname(__file__), 'resources') + os.path.join(os.path.dirname(__file__), 'resources'), ) diff --git a/pre_commit/manifest.py b/pre_commit/manifest.py index 081f3c603..c0c627fcf 100644 --- a/pre_commit/manifest.py +++ b/pre_commit/manifest.py @@ -31,7 +31,7 @@ def manifest_contents(self): 'If `pre-commit autoupdate` does not silence this warning ' 'consider making an issue / pull request.'.format( self.repo_url, C.MANIFEST_FILE_LEGACY, C.MANIFEST_FILE, - ) + ), ) return load_manifest(legacy_path) else: diff --git a/pre_commit/output.py b/pre_commit/output.py index 365960906..478ad5e65 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -50,7 +50,7 @@ def get_hook_message( raise ValueError('Expected one of (`end_msg`, `end_len`)') if end_msg is not None and (end_color is None or use_color is None): raise ValueError( - '`end_color` and `use_color` are required with `end_msg`' + '`end_color` and `use_color` are required with `end_msg`', ) if end_len: diff --git a/pre_commit/prefixed_command_runner.py b/pre_commit/prefixed_command_runner.py index 6ae850997..c2de526b2 100644 --- a/pre_commit/prefixed_command_runner.py +++ b/pre_commit/prefixed_command_runner.py @@ -20,7 +20,7 @@ def __init__( self, prefix_dir, popen=subprocess.Popen, - makedirs=os.makedirs + makedirs=os.makedirs, ): self.prefix_dir = prefix_dir.rstrip(os.sep) + os.sep self.__popen = popen diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 2c1eedb3d..fcc79bd6b 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -72,7 +72,7 @@ def _install_all(venvs, repo_url): if need_installed: logger.info( - 'Installing environment for {}.'.format(repo_url) + 'Installing environment for {}.'.format(repo_url), ) logger.info('Once installed this environment will be reused.') logger.info('This may take a few minutes...') @@ -102,7 +102,7 @@ def _validate_minimum_version(hook): 'version {} is installed. ' 'Perhaps run `pip install --upgrade pre-commit`.'.format( hook['id'], hook_version, C.VERSION_PARSED, - ) + ), ) exit(1) return hook @@ -147,7 +147,7 @@ def hooks(self): 'Typo? Perhaps it is introduced in a newer version? ' 'Often `pre-commit autoupdate` fixes this.'.format( hook['id'], self.repo_config['repo'], - ) + ), ) exit(1) diff --git a/pre_commit/schema.py b/pre_commit/schema.py index d34ad7373..5f22277d8 100644 --- a/pre_commit/schema.py +++ b/pre_commit/schema.py @@ -112,7 +112,7 @@ def _check_conditional(self, dct): cond_key=self.condition_key, op=op, cond_val=cond_val, - ) + ), ) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index d6ace66fb..862c6bd1a 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -49,7 +49,7 @@ def staged_files_only(cmd_runner): except CalledProcessError: logger.warning( 'Stashed changes conflicted with hook auto-fixes... ' - 'Rolling back fixes...' + 'Rolling back fixes...', ) # We failed to apply the patch, presumably due to fixes made # by hooks. diff --git a/pre_commit/store.py b/pre_commit/store.py index 67564483c..ee1f755b9 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -49,7 +49,7 @@ def _write_readme(self): with io.open(os.path.join(self.directory, 'README'), 'w') as readme: readme.write( 'This directory is maintained by the pre-commit project.\n' - 'Learn more: https://github.com/pre-commit/pre-commit\n' + 'Learn more: https://github.com/pre-commit/pre-commit\n', ) def _write_sqlite_db(self): @@ -68,7 +68,7 @@ def _write_sqlite_db(self): ' ref CHAR(255) NOT NULL,' ' path CHAR(255) NOT NULL,' ' PRIMARY KEY (repo, ref)' - ');' + ');', ) # Atomic file move diff --git a/pre_commit/util.py b/pre_commit/util.py index 4c3ad421b..b0095843b 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -124,7 +124,7 @@ def to_bytes(self): if maybe_text: output.append( b'\n ' + - five.to_bytes(maybe_text).replace(b'\n', b'\n ') + five.to_bytes(maybe_text).replace(b'\n', b'\n '), ) else: output.append(b'(none)') @@ -134,8 +134,8 @@ def to_bytes(self): 'Command: {!r}\n' 'Return code: {}\n' 'Expected return code: {}\n'.format( - self.cmd, self.returncode, self.expected_returncode - ) + self.cmd, self.returncode, self.expected_returncode, + ), ), b'Output: ', output[0], b'\n', b'Errors: ', output[1], b'\n', diff --git a/testing/fixtures.py b/testing/fixtures.py index dffff4ca0..4a3f1446b 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -75,7 +75,7 @@ def config_with_local_hooks(): ('entry', 'DO NOT COMMIT'), ('language', 'pcre'), ('files', '^(.*)$'), - ))]) + ))]), )) diff --git a/testing/util.py b/testing/util.py index 4d752f3e9..332b64185 100644 --- a/testing/util.py +++ b/testing/util.py @@ -34,7 +34,7 @@ def cmd_output_mocked_pre_commit_home(*args, **kwargs): skipif_cant_run_docker = pytest.mark.skipif( docker_is_running() is False, - reason='Docker isn\'t running or can\'t be accessed' + reason='Docker isn\'t running or can\'t be accessed', ) skipif_slowtests_false = pytest.mark.skipif( @@ -44,7 +44,7 @@ def cmd_output_mocked_pre_commit_home(*args, **kwargs): skipif_cant_run_swift = pytest.mark.skipif( parse_shebang.find_executable('swift') is None, - reason='swift isn\'t installed or can\'t be found' + reason='swift isn\'t installed or can\'t be found', ) xfailif_windows_no_ruby = pytest.mark.xfail( diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 65209a644..9e66025de 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -208,7 +208,7 @@ def test_validate_manifest_main(args, expected_output): }], True, ), - ) + ), ) def test_valid_manifests(manifest_obj, expected): ret = is_valid_according_to_schema(manifest_obj, MANIFEST_SCHEMA) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 550946b6f..8dac48c4c 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -105,7 +105,7 @@ def test_out_of_date_repo(out_of_date_repo, runner_with_mocked_store): def test_autoupdate_out_of_date_repo( - out_of_date_repo, in_tmpdir, mock_out_store_directory + out_of_date_repo, in_tmpdir, mock_out_store_directory, ): # Write out the config config = make_config_from_repo( @@ -181,7 +181,7 @@ def hook_disappearing_repo(tempdir_factory): def test_hook_disppearing_repo_raises( - hook_disappearing_repo, runner_with_mocked_store + hook_disappearing_repo, runner_with_mocked_store, ): config = make_config_from_repo( hook_disappearing_repo.path, @@ -193,7 +193,7 @@ def test_hook_disppearing_repo_raises( def test_autoupdate_hook_disappearing_repo( - hook_disappearing_repo, in_tmpdir, mock_out_store_directory + hook_disappearing_repo, in_tmpdir, mock_out_store_directory, ): config = make_config_from_repo( hook_disappearing_repo.path, @@ -222,7 +222,7 @@ def test_autoupdate_local_hooks(tempdir_factory): def test_autoupdate_local_hooks_with_out_of_date_repo( - out_of_date_repo, in_tmpdir, mock_out_store_directory + out_of_date_repo, in_tmpdir, mock_out_store_directory, ): stale_config = make_config_from_repo( out_of_date_repo.path, sha=out_of_date_repo.original_sha, check=False, diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index d00d55d77..1fb0f8f14 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -145,7 +145,7 @@ def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): r'Bash hook\.+Passed\r?\n' r'\[master [a-f0-9]{7}\] Commit!\r?\n' + FILES_CHANGED + - r' create mode 100644 foo\r?\n$' + r' create mode 100644 foo\r?\n$', ) @@ -259,7 +259,7 @@ def test_environment_not_sourced(tempdir_factory): r'\r?\n' r'Fail\r?\n' r'foo\r?\n' - r'\r?\n$' + r'\r?\n$', ) @@ -277,7 +277,7 @@ def test_failing_hooks_returns_nonzero(tempdir_factory): r'^legacy hook\r?\n' r'\[master [a-f0-9]{7}\] Commit!\r?\n' + FILES_CHANGED + - r' create mode 100644 baz\r?\n$' + r' create mode 100644 baz\r?\n$', ) @@ -332,7 +332,7 @@ def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory): FAIL_OLD_HOOK = re.compile( r'fail!\r?\n' r'\[INFO\] Initializing environment for .+\.\r?\n' - r'Bash hook\.+Passed\r?\n' + r'Bash hook\.+Passed\r?\n', ) @@ -448,7 +448,7 @@ def test_uninstall_doesnt_remove_not_our_hooks(tempdir_factory): r'Bash hook\.+Passed\r?\n' r'\[master [a-f0-9]{7}\] Commit!\r?\n' + FILES_CHANGED + - r' create mode 100644 foo\r?\n$' + r' create mode 100644 foo\r?\n$', ) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 1643cbb81..01164a632 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -220,7 +220,7 @@ def test_show_diff_on_failure( True, ), ({}, (b'Bash hook', b'(no files to check)', b'Skipped'), 0, False), - ) + ), ) def test_run( cap_out, @@ -259,7 +259,7 @@ def test_run_output_logfile( git_path, {}, expected_output, expected_ret=1, - stage=True + stage=True, ) logfile_path = os.path.join(git_path, 'test.log') assert os.path.exists(logfile_path) @@ -301,7 +301,7 @@ def test_always_run_alt_config( (b'Bash hook', b'Passed'), 0, stage=False, - config_file=alt_config_file + config_file=alt_config_file, ) @@ -311,7 +311,7 @@ def test_always_run_alt_config( ('master', 'master', False), ('master', '', True), ('', 'master', True), - ) + ), ) def test_origin_source_error_msg( repo_with_passing_hook, origin, source, expect_failure, @@ -588,7 +588,7 @@ def test_lots_of_files(mock_out_store_directory, tempdir_factory): ('commit', [], ['commit'], [b'hook 1', b'hook 2']), ('commit', ['push'], ['commit'], [b'', b'hook 2']), ('commit', ['commit'], ['push'], [b'hook 1', b'']), - ) + ), ) def test_local_hook_for_stages( cap_out, @@ -606,15 +606,15 @@ def test_local_hook_for_stages( ('entry', 'python -m flake8.__main__'), ('language', 'system'), ('files', r'\.py$'), - ('stages', stage_for_first_hook) + ('stages', stage_for_first_hook), )), OrderedDict(( ('id', 'do_not_commit'), ('name', 'hook 2'), ('entry', 'DO NOT COMMIT'), ('language', 'pcre'), ('files', '^(.*)$'), - ('stages', stage_for_second_hook) - )))) + ('stages', stage_for_second_hook), + )))), )) add_config_to_repo(repo_with_passing_hook, config) @@ -628,7 +628,7 @@ def test_local_hook_for_stages( {'hook_stage': hook_stage}, expected_outputs=expected_output, expected_ret=0, - stage=False + stage=False, ) @@ -649,7 +649,7 @@ def test_local_hook_passes( ('entry', 'DO NOT COMMIT'), ('language', 'pcre'), ('files', '^(.*)$'), - )))) + )))), )) add_config_to_repo(repo_with_passing_hook, config) @@ -678,7 +678,7 @@ def test_local_hook_fails( ('entry', 'sh -c "! grep -iI todo $@" --'), ('language', 'system'), ('files', ''), - ))]) + ))]), )) add_config_to_repo(repo_with_passing_hook, config) @@ -770,7 +770,7 @@ def test_files_running_subdir( (False, [], b''), (True, ['some', 'args'], b'some args foo.py'), (False, ['some', 'args'], b'some args'), - ) + ), ) def test_pass_filenames( cap_out, repo_with_passing_hook, mock_out_store_directory, diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 6ca2ed5cc..9f7f55cf2 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -10,6 +10,6 @@ def test_docker_is_running_process_error(): with mock.patch( 'pre_commit.languages.docker.cmd_output', - side_effect=CalledProcessError(*(None,) * 4) + side_effect=CalledProcessError(*(None,) * 4), ): assert docker.docker_is_running() is False diff --git a/tests/prefixed_command_runner_test.py b/tests/prefixed_command_runner_test.py index 132c2a86f..41b436c13 100644 --- a/tests/prefixed_command_runner_test.py +++ b/tests/prefixed_command_runner_test.py @@ -31,7 +31,7 @@ def test_CalledProcessError_str(): def test_CalledProcessError_str_nooutput(): error = CalledProcessError( - 1, [str('git'), str('status')], 0, (str(''), str('')) + 1, [str('git'), str('status')], 0, (str(''), str('')), ) assert str(error) == ( "Command: ['git', 'status']\n" diff --git a/tests/repository_test.py b/tests/repository_test.py index 7131d75b9..9096161e9 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -62,7 +62,7 @@ def test_python_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'python_hooks_repo', 'foo', [os.devnull], - b"['" + five.to_bytes(os.devnull) + b"']\nHello World\n" + b"['" + five.to_bytes(os.devnull) + b"']\nHello World\n", ) @@ -78,7 +78,7 @@ def test_python_hook_args_with_spaces(tempdir_factory, store): 'hooks': [{ 'id': 'foo', 'args': ['i have spaces', 'and"\'quotes', '$and !this'], - }] + }], }, ) @@ -93,7 +93,7 @@ def test_python_hook_weird_setup_cfg(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'python_hooks_repo', 'foo', [os.devnull], - b"['" + five.to_bytes(os.devnull) + b"']\nHello World\n" + b"['" + five.to_bytes(os.devnull) + b"']\nHello World\n", ) @@ -160,7 +160,7 @@ def test_run_a_failing_docker_hook(tempdir_factory, store): tempdir_factory, store, 'docker_hooks_repo', 'docker-hook-failing', ['Hello World from docker'], b'', - expected_return_code=1 + expected_return_code=1, ) @@ -219,7 +219,7 @@ def test_run_ruby_hook_with_disable_shared_gems( tmpdir.join('.bundle').mkdir() tmpdir.join('.bundle', 'config').write( 'BUNDLE_DISABLE_SHARED_GEMS: true\n' - 'BUNDLE_PATH: vendor/gem\n' + 'BUNDLE_PATH: vendor/gem\n', ) with cwd(tmpdir.strpath): _test_hook_repo( @@ -322,7 +322,7 @@ def test_run_hook_with_curly_braced_arguments(tempdir_factory, store): 'hooks': [{ 'id': 'arg-per-line', 'args': ['hi {1}', "I'm {a} problem"], - }] + }], }, ) diff --git a/tests/runner_test.py b/tests/runner_test.py index a4f8cb7c0..eb1f48ef3 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -70,7 +70,7 @@ def test_local_hooks(tempdir_factory, mock_out_store_directory): ('entry', 'DO NOT COMMIT'), ('language', 'pcre'), ('files', '^(.*)$'), - )))) + )))), )) git_path = git_dir(tempdir_factory) add_config_to_repo(git_path, config) @@ -101,7 +101,7 @@ def test_local_hooks_alt_config(tempdir_factory, mock_out_store_directory): ('entry', 'DO NOT COMMIT'), ('language', 'pcre'), ('files', '^(.*)$'), - )))) + )))), )) git_path = git_dir(tempdir_factory) alt_config_file = 'alternate_config.yaml' diff --git a/tests/schema_test.py b/tests/schema_test.py index 914e60977..c2ecf0faa 100644 --- a/tests/schema_test.py +++ b/tests/schema_test.py @@ -366,7 +366,7 @@ def test_load_from_filename_fails_load_strategy(tmpdir): _assert_exception_trace( excinfo.value.args[0], # ANY is json's error message - ('File {}'.format(f.strpath), mock.ANY) + ('File {}'.format(f.strpath), mock.ANY), ) diff --git a/tests/store_test.py b/tests/store_test.py index 1bbcf44a2..eab4b009a 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -34,7 +34,7 @@ def test_get_default_directory_defaults_to_home(): def test_uses_environment_variable_when_present(): with mock.patch.dict( - os.environ, {'PRE_COMMIT_HOME': '/tmp/pre_commit_home'} + os.environ, {'PRE_COMMIT_HOME': '/tmp/pre_commit_home'}, ): ret = _get_default_directory() assert ret == '/tmp/pre_commit_home' @@ -89,7 +89,7 @@ def test_clone(store, tempdir_factory, log_info_mock): ret = store.clone(path, sha) # Should have printed some stuff assert log_info_mock.call_args_list[0][0][0].startswith( - 'Initializing environment for ' + 'Initializing environment for ', ) # Should return a directory inside of the store @@ -138,7 +138,7 @@ def test_clone_when_repo_already_exists(store): with sqlite3.connect(store.db_path) as db: db.execute( 'INSERT INTO repos (repo, ref, path) ' - 'VALUES ("fake_repo", "fake_ref", "fake_path")' + 'VALUES ("fake_repo", "fake_ref", "fake_path")', ) assert store.clone('fake_repo', 'fake_ref') == 'fake_path' From 5c0783b2d0668cc9751735e0f7eea1e2f79c1448 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 12 Jul 2017 18:59:51 -0700 Subject: [PATCH 010/544] Try picking a pypy --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 5ce1af6c5..900446d29 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,7 @@ matrix: - env: TOXENV=py36 python: 3.6 - env: TOXENV=pypy + python: pypy-5.7.1 install: pip install coveralls tox script: tox before_install: From 86d9ca053beb7ca17003255ac680937111f12872 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 9 Jul 2017 10:19:42 -0700 Subject: [PATCH 011/544] Remove legacy 'hooks.yaml' file support --- hooks.yaml | 12 ------------ pre_commit/constants.py | 3 --- pre_commit/manifest.py | 17 +---------------- testing/fixtures.py | 7 ++----- .../resources/legacy_hooks_yaml_repo/hooks.yaml | 5 ----- tests/manifest_test.py | 17 ----------------- tests/meta_test.py | 9 --------- tests/repository_test.py | 9 --------- 8 files changed, 3 insertions(+), 76 deletions(-) delete mode 100644 hooks.yaml delete mode 100644 testing/resources/legacy_hooks_yaml_repo/hooks.yaml delete mode 100644 tests/meta_test.py diff --git a/hooks.yaml b/hooks.yaml deleted file mode 100644 index af53043ed..000000000 --- a/hooks.yaml +++ /dev/null @@ -1,12 +0,0 @@ -- id: validate_config - name: Validate Pre-Commit Config - description: This validator validates a pre-commit hooks config file - entry: pre-commit-validate-config - language: python - files: ^\.pre-commit-config\.yaml$ -- id: validate_manifest - name: Validate Pre-Commit Manifest - description: This validator validates a pre-commit hooks manifest file - entry: pre-commit-validate-manifest - language: python - files: ^(\.pre-commit-hooks\.yaml|hooks\.yaml)$ diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 3f81c8020..8af491846 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -3,10 +3,7 @@ import pkg_resources CONFIG_FILE = '.pre-commit-config.yaml' - -# In 0.12.0, the default file was changed to be namespaced MANIFEST_FILE = '.pre-commit-hooks.yaml' -MANIFEST_FILE_LEGACY = 'hooks.yaml' YAML_DUMP_KWARGS = { 'default_flow_style': False, diff --git a/pre_commit/manifest.py b/pre_commit/manifest.py index c0c627fcf..df2884428 100644 --- a/pre_commit/manifest.py +++ b/pre_commit/manifest.py @@ -20,22 +20,7 @@ def __init__(self, repo_path, repo_url): @cached_property def manifest_contents(self): - default_path = os.path.join(self.repo_path, C.MANIFEST_FILE) - legacy_path = os.path.join(self.repo_path, C.MANIFEST_FILE_LEGACY) - if os.path.exists(legacy_path) and not os.path.exists(default_path): - logger.warning( - '{} uses legacy {} to provide hooks.\n' - 'In newer versions, this file is called {}\n' - 'This will work in this version of pre-commit but will be ' - 'removed at a later time.\n' - 'If `pre-commit autoupdate` does not silence this warning ' - 'consider making an issue / pull request.'.format( - self.repo_url, C.MANIFEST_FILE_LEGACY, C.MANIFEST_FILE, - ), - ) - return load_manifest(legacy_path) - else: - return load_manifest(default_path) + return load_manifest(os.path.join(self.repo_path, C.MANIFEST_FILE)) @cached_property def hooks(self): diff --git a/testing/fixtures.py b/testing/fixtures.py index 4a3f1446b..794720f2f 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -79,11 +79,8 @@ def config_with_local_hooks(): )) -def make_config_from_repo( - repo_path, sha=None, hooks=None, check=True, legacy=False, -): - filename = C.MANIFEST_FILE_LEGACY if legacy else C.MANIFEST_FILE - manifest = load_manifest(os.path.join(repo_path, filename)) +def make_config_from_repo(repo_path, sha=None, hooks=None, check=True): + manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) config = OrderedDict(( ('repo', repo_path), ('sha', sha or get_head_sha(repo_path)), diff --git a/testing/resources/legacy_hooks_yaml_repo/hooks.yaml b/testing/resources/legacy_hooks_yaml_repo/hooks.yaml deleted file mode 100644 index b2c347c14..000000000 --- a/testing/resources/legacy_hooks_yaml_repo/hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: system-hook-with-spaces - name: System hook with spaces - entry: bash -c 'echo "Hello World"' - language: system - files: \.sh$ diff --git a/tests/manifest_test.py b/tests/manifest_test.py index ada004fcc..ee1857c9a 100644 --- a/tests/manifest_test.py +++ b/tests/manifest_test.py @@ -60,23 +60,6 @@ def test_hooks(manifest): } -def test_legacy_manifest_warn(store, tempdir_factory, log_warning_mock): - path = make_repo(tempdir_factory, 'legacy_hooks_yaml_repo') - head_sha = get_head_sha(path) - repo_path = store.clone(path, head_sha) - Manifest(repo_path, path).manifest_contents - - # Should have printed a warning - assert log_warning_mock.call_args_list[0][0][0] == ( - '{} uses legacy hooks.yaml to provide hooks.\n' - 'In newer versions, this file is called .pre-commit-hooks.yaml\n' - 'This will work in this version of pre-commit but will be removed at ' - 'a later time.\n' - 'If `pre-commit autoupdate` does not silence this warning consider ' - 'making an issue / pull request.'.format(path) - ) - - def test_default_python_language_version(store, tempdir_factory): path = make_repo(tempdir_factory, 'python_hooks_repo') repo_path = store.clone(path, get_head_sha(path)) diff --git a/tests/meta_test.py b/tests/meta_test.py deleted file mode 100644 index 64cea2623..000000000 --- a/tests/meta_test.py +++ /dev/null @@ -1,9 +0,0 @@ -import io - -import pre_commit.constants as C - - -def test_hooks_yaml_same_contents(): - legacy_contents = io.open(C.MANIFEST_FILE_LEGACY).read() - contents = io.open(C.MANIFEST_FILE).read() - assert legacy_contents == contents diff --git a/tests/repository_test.py b/tests/repository_test.py index 9096161e9..7c4009def 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -238,15 +238,6 @@ def test_system_hook_with_spaces(tempdir_factory, store): ) -@pytest.mark.integration -def test_repo_with_legacy_hooks_yaml(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'legacy_hooks_yaml_repo', - 'system-hook-with-spaces', ['/dev/null'], b'Hello World\n', - config_kwargs={'legacy': True}, - ) - - @skipif_cant_run_swift @pytest.mark.integration def test_swift_hook(tempdir_factory, store): From be3fbdf94ee7925dcb362de75fe105d82a7e637e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 15 Jul 2017 12:31:29 -0700 Subject: [PATCH 012/544] Upgrade add-trailing-comma to 0.4.0 --- .pre-commit-config.yaml | 2 +- CONTRIBUTING.md | 23 ---- pre_commit/languages/ruby.py | 12 +- testing/fixtures.py | 16 +-- tests/clientlib_test.py | 156 +++++++++++++------------- tests/color_test.py | 10 +- tests/commands/run_test.py | 104 ++++++++++------- tests/prefixed_command_runner_test.py | 16 +-- tests/runner_test.py | 76 +++++++------ 9 files changed, 216 insertions(+), 199 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 46a47341a..4bca47fec 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,6 +22,6 @@ - id: reorder-python-imports language_version: python2.7 - repo: https://github.com/asottile/add-trailing-comma - sha: v0.3.0 + sha: v0.4.0 hooks: - id: add-trailing-comma diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1e0b2460c..ae4511f7d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,29 +46,6 @@ variable `slowtests=false`. With the environment activated simply run `pre-commit install`. -## Style - -This repository follows pep8 (and enforces it with flake8). There are a few -nitpicky things I also like that I'll outline below. - -### Multi-line method invocation - -Multiple line method invocation should look as follows - -```python -function_call( - argument, - argument, - argument, -) -``` - -Some notable features: -- The initial parenthesis is at the end of the line -- Parameters are indented one indentation level further than the function name -- The last parameter contains a trailing comma (This helps make `git blame` - more accurate and reduces merge conflicts when adding / removing parameters). - ## Documentation Documentation is hosted at http://pre-commit.com diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 6a0bde277..41c13a87f 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -24,11 +24,13 @@ def get_env_patch(venv, language_version): # pragma: windows no cover ('GEM_HOME', os.path.join(venv, 'gems')), ('RBENV_ROOT', venv), ('BUNDLE_IGNORE_CONFIG', '1'), - ('PATH', ( - os.path.join(venv, 'gems', 'bin'), os.pathsep, - os.path.join(venv, 'shims'), os.pathsep, - os.path.join(venv, 'bin'), os.pathsep, Var('PATH'), - )), + ( + 'PATH', ( + os.path.join(venv, 'gems', 'bin'), os.pathsep, + os.path.join(venv, 'shims'), os.pathsep, + os.path.join(venv, 'bin'), os.pathsep, Var('PATH'), + ), + ), ) if language_version != 'default': patches += (('RBENV_VERSION', language_version),) diff --git a/testing/fixtures.py b/testing/fixtures.py index 4a3f1446b..be4342165 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -69,13 +69,15 @@ def modify_config(path='.', commit=True): def config_with_local_hooks(): return OrderedDict(( ('repo', 'local'), - ('hooks', [OrderedDict(( - ('id', 'do_not_commit'), - ('name', 'Block if "DO NOT COMMIT" is found'), - ('entry', 'DO NOT COMMIT'), - ('language', 'pcre'), - ('files', '^(.*)$'), - ))]), + ( + 'hooks', [OrderedDict(( + ('id', 'do_not_commit'), + ('name', 'Block if "DO NOT COMMIT" is found'), + ('entry', 'DO NOT COMMIT'), + ('language', 'pcre'), + ('files', '^(.*)$'), + ))], + ), )) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 9e66025de..1fe1d80b6 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -56,94 +56,100 @@ def test_validate_config_main(args, expected_output): assert validate_config_main(args) == expected_output -@pytest.mark.parametrize(('config_obj', 'expected'), ( - ([], False), - ( - [{ - 'repo': 'git@github.com:pre-commit/pre-commit-hooks', - 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', - 'hooks': [{'id': 'pyflakes', 'files': '\\.py$'}], - }], - True, - ), - ( - [{ - 'repo': 'git@github.com:pre-commit/pre-commit-hooks', - 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', - 'hooks': [ - { - 'id': 'pyflakes', - 'files': '\\.py$', - 'args': ['foo', 'bar', 'baz'], - }, - ], - }], - True, - ), - ( - [{ - 'repo': 'git@github.com:pre-commit/pre-commit-hooks', - 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', - 'hooks': [ - { - 'id': 'pyflakes', - 'files': '\\.py$', - # Exclude pattern must be a string - 'exclude': 0, - 'args': ['foo', 'bar', 'baz'], - }, - ], - }], - False, +@pytest.mark.parametrize( + ('config_obj', 'expected'), ( + ([], False), + ( + [{ + 'repo': 'git@github.com:pre-commit/pre-commit-hooks', + 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', + 'hooks': [{'id': 'pyflakes', 'files': '\\.py$'}], + }], + True, + ), + ( + [{ + 'repo': 'git@github.com:pre-commit/pre-commit-hooks', + 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', + 'hooks': [ + { + 'id': 'pyflakes', + 'files': '\\.py$', + 'args': ['foo', 'bar', 'baz'], + }, + ], + }], + True, + ), + ( + [{ + 'repo': 'git@github.com:pre-commit/pre-commit-hooks', + 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', + 'hooks': [ + { + 'id': 'pyflakes', + 'files': '\\.py$', + # Exclude pattern must be a string + 'exclude': 0, + 'args': ['foo', 'bar', 'baz'], + }, + ], + }], + False, + ), ), -)) +) def test_config_valid(config_obj, expected): ret = is_valid_according_to_schema(config_obj, CONFIG_SCHEMA) assert ret is expected -@pytest.mark.parametrize('config_obj', ( - [{ - 'repo': 'local', - 'sha': 'foo', - 'hooks': [{ - 'id': 'do_not_commit', - 'name': 'Block if "DO NOT COMMIT" is found', - 'entry': 'DO NOT COMMIT', - 'language': 'pcre', - 'files': '^(.*)$', +@pytest.mark.parametrize( + 'config_obj', ( + [{ + 'repo': 'local', + 'sha': 'foo', + 'hooks': [{ + 'id': 'do_not_commit', + 'name': 'Block if "DO NOT COMMIT" is found', + 'entry': 'DO NOT COMMIT', + 'language': 'pcre', + 'files': '^(.*)$', + }], }], - }], -)) + ), +) def test_config_with_local_hooks_definition_fails(config_obj): with pytest.raises(schema.ValidationError): schema.validate(config_obj, CONFIG_SCHEMA) -@pytest.mark.parametrize('config_obj', ( - [{ - 'repo': 'local', - 'hooks': [{ - 'id': 'arg-per-line', - 'name': 'Args per line hook', - 'entry': 'bin/hook.sh', - 'language': 'script', - 'files': '', - 'args': ['hello', 'world'], +@pytest.mark.parametrize( + 'config_obj', ( + [{ + 'repo': 'local', + 'hooks': [{ + 'id': 'arg-per-line', + 'name': 'Args per line hook', + 'entry': 'bin/hook.sh', + 'language': 'script', + 'files': '', + 'args': ['hello', 'world'], + }], }], - }], - [{ - 'repo': 'local', - 'hooks': [{ - 'id': 'arg-per-line', - 'name': 'Args per line hook', - 'entry': 'bin/hook.sh', - 'language': 'script', - 'files': '', - 'args': ['hello', 'world'], - }] - }], -)) + [{ + 'repo': 'local', + 'hooks': [{ + 'id': 'arg-per-line', + 'name': 'Args per line hook', + 'entry': 'bin/hook.sh', + 'language': 'script', + 'files': '', + 'args': ['hello', 'world'], + }] + }], + ), +) def test_config_with_local_hooks_definition_passes(config_obj): schema.validate(config_obj, CONFIG_SCHEMA) diff --git a/tests/color_test.py b/tests/color_test.py index 4fb7676ad..0b8a4d699 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -11,10 +11,12 @@ from pre_commit.color import use_color -@pytest.mark.parametrize(('in_text', 'in_color', 'in_use_color', 'expected'), ( - ('foo', GREEN, True, '{}foo\033[0m'.format(GREEN)), - ('foo', GREEN, False, 'foo'), -)) +@pytest.mark.parametrize( + ('in_text', 'in_color', 'in_use_color', 'expected'), ( + ('foo', GREEN, True, '{}foo\033[0m'.format(GREEN)), + ('foo', GREEN, False, 'foo'), + ), +) def test_format_color(in_text, in_color, in_use_color, expected): ret = format_color(in_text, in_color, in_use_color) assert ret == expected diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 01164a632..59f2ec9a6 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -86,8 +86,10 @@ def _do_run(cap_out, repo, args, environ={}, config_file=C.CONFIG_FILE): return ret, printed -def _test_run(cap_out, repo, opts, expected_outputs, expected_ret, stage, - config_file=C.CONFIG_FILE): +def _test_run( + cap_out, repo, opts, expected_outputs, expected_ret, stage, + config_file=C.CONFIG_FILE, +): if stage: stage_a_file() args = _get_opts(**opts) @@ -571,18 +573,24 @@ def test_lots_of_files(mock_out_store_directory, tempdir_factory): @pytest.mark.parametrize( - ('hook_stage', 'stage_for_first_hook', 'stage_for_second_hook', - 'expected_output'), + ( + 'hook_stage', 'stage_for_first_hook', 'stage_for_second_hook', + 'expected_output', + ), ( ('push', ['commit'], ['commit'], [b'', b'']), - ('push', ['commit', 'push'], ['commit', 'push'], - [b'hook 1', b'hook 2']), + ( + 'push', ['commit', 'push'], ['commit', 'push'], + [b'hook 1', b'hook 2'], + ), ('push', [], [], [b'hook 1', b'hook 2']), ('push', [], ['commit'], [b'hook 1', b'']), ('push', ['push'], ['commit'], [b'hook 1', b'']), ('push', ['commit'], ['push'], [b'', b'hook 2']), - ('commit', ['commit', 'push'], ['commit', 'push'], - [b'hook 1', b'hook 2']), + ( + 'commit', ['commit', 'push'], ['commit', 'push'], + [b'hook 1', b'hook 2'], + ), ('commit', ['commit'], ['commit'], [b'hook 1', b'hook 2']), ('commit', [], [], [b'hook 1', b'hook 2']), ('commit', [], ['commit'], [b'hook 1', b'hook 2']), @@ -600,21 +608,25 @@ def test_local_hook_for_stages( ): config = OrderedDict(( ('repo', 'local'), - ('hooks', (OrderedDict(( - ('id', 'flake8'), - ('name', 'hook 1'), - ('entry', 'python -m flake8.__main__'), - ('language', 'system'), - ('files', r'\.py$'), - ('stages', stage_for_first_hook), - )), OrderedDict(( - ('id', 'do_not_commit'), - ('name', 'hook 2'), - ('entry', 'DO NOT COMMIT'), - ('language', 'pcre'), - ('files', '^(.*)$'), - ('stages', stage_for_second_hook), - )))), + ( + 'hooks', ( + OrderedDict(( + ('id', 'flake8'), + ('name', 'hook 1'), + ('entry', 'python -m flake8.__main__'), + ('language', 'system'), + ('files', r'\.py$'), + ('stages', stage_for_first_hook), + )), OrderedDict(( + ('id', 'do_not_commit'), + ('name', 'hook 2'), + ('entry', 'DO NOT COMMIT'), + ('language', 'pcre'), + ('files', '^(.*)$'), + ('stages', stage_for_second_hook), + )), + ), + ), )) add_config_to_repo(repo_with_passing_hook, config) @@ -637,19 +649,23 @@ def test_local_hook_passes( ): config = OrderedDict(( ('repo', 'local'), - ('hooks', (OrderedDict(( - ('id', 'flake8'), - ('name', 'flake8'), - ('entry', 'python -m flake8.__main__'), - ('language', 'system'), - ('files', r'\.py$'), - )), OrderedDict(( - ('id', 'do_not_commit'), - ('name', 'Block if "DO NOT COMMIT" is found'), - ('entry', 'DO NOT COMMIT'), - ('language', 'pcre'), - ('files', '^(.*)$'), - )))), + ( + 'hooks', ( + OrderedDict(( + ('id', 'flake8'), + ('name', 'flake8'), + ('entry', 'python -m flake8.__main__'), + ('language', 'system'), + ('files', r'\.py$'), + )), OrderedDict(( + ('id', 'do_not_commit'), + ('name', 'Block if "DO NOT COMMIT" is found'), + ('entry', 'DO NOT COMMIT'), + ('language', 'pcre'), + ('files', '^(.*)$'), + )), + ), + ), )) add_config_to_repo(repo_with_passing_hook, config) @@ -672,13 +688,15 @@ def test_local_hook_fails( ): config = OrderedDict(( ('repo', 'local'), - ('hooks', [OrderedDict(( - ('id', 'no-todo'), - ('name', 'No TODO'), - ('entry', 'sh -c "! grep -iI todo $@" --'), - ('language', 'system'), - ('files', ''), - ))]), + ( + 'hooks', [OrderedDict(( + ('id', 'no-todo'), + ('name', 'No TODO'), + ('entry', 'sh -c "! grep -iI todo $@" --'), + ('language', 'system'), + ('files', ''), + ))], + ), )) add_config_to_repo(repo_with_passing_hook, config) diff --git a/tests/prefixed_command_runner_test.py b/tests/prefixed_command_runner_test.py index 41b436c13..c928dc8af 100644 --- a/tests/prefixed_command_runner_test.py +++ b/tests/prefixed_command_runner_test.py @@ -54,13 +54,15 @@ def makedirs_mock(): return mock.Mock(spec=os.makedirs) -@pytest.mark.parametrize(('input', 'expected_prefix'), ( - norm_slash(('.', './')), - norm_slash(('foo', 'foo/')), - norm_slash(('bar/', 'bar/')), - norm_slash(('foo/bar', 'foo/bar/')), - norm_slash(('foo/bar/', 'foo/bar/')), -)) +@pytest.mark.parametrize( + ('input', 'expected_prefix'), ( + norm_slash(('.', './')), + norm_slash(('foo', 'foo/')), + norm_slash(('bar/', 'bar/')), + norm_slash(('foo/bar', 'foo/bar/')), + norm_slash(('foo/bar/', 'foo/bar/')), + ), +) def test_init_normalizes_path_endings(input, expected_prefix): input = input.replace('/', os.sep) expected_prefix = expected_prefix.replace('/', os.sep) diff --git a/tests/runner_test.py b/tests/runner_test.py index eb1f48ef3..0201156c5 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -57,20 +57,24 @@ def test_repositories(tempdir_factory, mock_out_store_directory): def test_local_hooks(tempdir_factory, mock_out_store_directory): config = OrderedDict(( ('repo', 'local'), - ('hooks', (OrderedDict(( - ('id', 'arg-per-line'), - ('name', 'Args per line hook'), - ('entry', 'bin/hook.sh'), - ('language', 'script'), - ('files', ''), - ('args', ['hello', 'world']), - )), OrderedDict(( - ('id', 'do_not_commit'), - ('name', 'Block if "DO NOT COMMIT" is found'), - ('entry', 'DO NOT COMMIT'), - ('language', 'pcre'), - ('files', '^(.*)$'), - )))), + ( + 'hooks', ( + OrderedDict(( + ('id', 'arg-per-line'), + ('name', 'Args per line hook'), + ('entry', 'bin/hook.sh'), + ('language', 'script'), + ('files', ''), + ('args', ['hello', 'world']), + )), OrderedDict(( + ('id', 'do_not_commit'), + ('name', 'Block if "DO NOT COMMIT" is found'), + ('entry', 'DO NOT COMMIT'), + ('language', 'pcre'), + ('files', '^(.*)$'), + )), + ), + ), )) git_path = git_dir(tempdir_factory) add_config_to_repo(git_path, config) @@ -82,26 +86,30 @@ def test_local_hooks(tempdir_factory, mock_out_store_directory): def test_local_hooks_alt_config(tempdir_factory, mock_out_store_directory): config = OrderedDict(( ('repo', 'local'), - ('hooks', (OrderedDict(( - ('id', 'arg-per-line'), - ('name', 'Args per line hook'), - ('entry', 'bin/hook.sh'), - ('language', 'script'), - ('files', ''), - ('args', ['hello', 'world']), - )), OrderedDict(( - ('id', 'ugly-format-json'), - ('name', 'Ugly format json'), - ('entry', 'ugly-format-json'), - ('language', 'python'), - ('files', ''), - )), OrderedDict(( - ('id', 'do_not_commit'), - ('name', 'Block if "DO NOT COMMIT" is found'), - ('entry', 'DO NOT COMMIT'), - ('language', 'pcre'), - ('files', '^(.*)$'), - )))), + ( + 'hooks', ( + OrderedDict(( + ('id', 'arg-per-line'), + ('name', 'Args per line hook'), + ('entry', 'bin/hook.sh'), + ('language', 'script'), + ('files', ''), + ('args', ['hello', 'world']), + )), OrderedDict(( + ('id', 'ugly-format-json'), + ('name', 'Ugly format json'), + ('entry', 'ugly-format-json'), + ('language', 'python'), + ('files', ''), + )), OrderedDict(( + ('id', 'do_not_commit'), + ('name', 'Block if "DO NOT COMMIT" is found'), + ('entry', 'DO NOT COMMIT'), + ('language', 'pcre'), + ('files', '^(.*)$'), + )), + ), + ), )) git_path = git_dir(tempdir_factory) alt_config_file = 'alternate_config.yaml' From 3e3932d5a670a767c9a3f73d2b5d9351f8559c55 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 17 Jul 2017 17:39:55 -0700 Subject: [PATCH 013/544] Upgrade add-trailing-comma to 0.5.1 --- .pre-commit-config.yaml | 2 +- tests/clientlib_test.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4bca47fec..3f1de932e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,6 +22,6 @@ - id: reorder-python-imports language_version: python2.7 - repo: https://github.com/asottile/add-trailing-comma - sha: v0.4.0 + sha: v0.5.1 hooks: - id: add-trailing-comma diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 1fe1d80b6..6c04648c7 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -146,7 +146,7 @@ def test_config_with_local_hooks_definition_fails(config_obj): 'language': 'script', 'files': '', 'args': ['hello', 'world'], - }] + }], }], ), ) @@ -185,7 +185,7 @@ def test_validate_manifest_main(args, expected_output): 'name': 'b', 'entry': 'c', 'language': 'python', - 'files': r'\.py$' + 'files': r'\.py$', }], True, ), From 64dd893ab41d7ac68eee0350b247e310d5a02980 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Jul 2017 11:21:03 -0700 Subject: [PATCH 014/544] Upgrade add-trailing-comma --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3f1de932e..af8f1a7ce 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,6 +22,6 @@ - id: reorder-python-imports language_version: python2.7 - repo: https://github.com/asottile/add-trailing-comma - sha: v0.5.1 + sha: v0.6.1 hooks: - id: add-trailing-comma From cff98a634dc8405e985cb04fe7230c6fe6171b47 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Jul 2017 14:23:39 -0700 Subject: [PATCH 015/544] Recover from invalid python virtualenvs --- pre_commit/languages/all.py | 3 +++ pre_commit/languages/docker.py | 1 + pre_commit/languages/golang.py | 1 + pre_commit/languages/helpers.py | 4 ++++ pre_commit/languages/node.py | 1 + pre_commit/languages/pcre.py | 1 + pre_commit/languages/python.py | 10 +++++++++- pre_commit/languages/ruby.py | 1 + pre_commit/languages/script.py | 1 + pre_commit/languages/swift.py | 1 + pre_commit/languages/system.py | 1 + pre_commit/repository.py | 12 +++++++----- tests/languages/all_test.py | 10 ++++++++++ tests/languages/helpers_test.py | 12 ++++++++++++ tests/repository_test.py | 23 +++++++++++++++++++++++ 15 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 tests/languages/helpers_test.py diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 5546025d8..5de57fb8e 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -21,6 +21,9 @@ # return 'default' if there is no better option. # """ # +# def healthy(repo_cmd_runner, language_version): +# """Return whether or not the environment is considered functional.""" +# # def install_environment(repo_cmd_runner, version, additional_dependencies): # """Installs a repository in the given repository. Note that the current # working directory will already be inside the repository. diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 8404ac84c..a9a0d342f 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -15,6 +15,7 @@ ENVIRONMENT_DIR = 'docker' PRE_COMMIT_LABEL = 'PRE_COMMIT' get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy def md5(s): # pragma: windows no cover diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index ee04ca796..c091bacfd 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -15,6 +15,7 @@ ENVIRONMENT_DIR = 'golangenv' get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy def get_env_patch(venv): diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 6af77e307..930a0755b 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -37,3 +37,7 @@ def assert_no_additional_deps(lang, additional_deps): def basic_get_default_version(): return 'default' + + +def basic_healthy(repo_cmd_runner, language_version): + return True diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index b5f7c56e0..69378b068 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -13,6 +13,7 @@ ENVIRONMENT_DIR = 'node_env' get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy def get_env_patch(venv): # pragma: windows no cover diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py index faba53957..6ef373f07 100644 --- a/pre_commit/languages/pcre.py +++ b/pre_commit/languages/pcre.py @@ -9,6 +9,7 @@ ENVIRONMENT_DIR = None GREP = 'ggrep' if sys.platform == 'darwin' else 'grep' get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy def install_environment(repo_cmd_runner, version, additional_dependencies): diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 11f377654..7800e17a6 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -10,11 +10,11 @@ from pre_commit.languages import helpers from pre_commit.parse_shebang import find_executable from pre_commit.util import clean_path_on_failure +from pre_commit.util import cmd_output from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'py_env' -get_default_version = helpers.basic_get_default_version def bin_dir(venv): @@ -83,6 +83,14 @@ def get_default_version(): return get_default_version() +def healthy(repo_cmd_runner, language_version): + with in_env(repo_cmd_runner, language_version): + retcode, _, _ = cmd_output( + 'python', '-c', 'import datetime, io, os, weakref', retcode=None, + ) + return retcode == 0 + + def norm_version(version): if os.name == 'nt': # pragma: no cover (windows) # Try looking up by name diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 41c13a87f..e7e0c3286 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -17,6 +17,7 @@ ENVIRONMENT_DIR = 'rbenv' get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy def get_env_patch(venv, language_version): # pragma: windows no cover diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index c4b6593d5..0bbb3091b 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -6,6 +6,7 @@ ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy def install_environment(repo_cmd_runner, version, additional_dependencies): diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index a27dfac24..f4d1eb5a1 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -11,6 +11,7 @@ ENVIRONMENT_DIR = 'swift_env' get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy BUILD_DIR = '.build' BUILD_CONFIG = 'release' diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 31480792c..1f1688d8d 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -6,6 +6,7 @@ ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy def install_environment(repo_cmd_runner, version, additional_dependencies): diff --git a/pre_commit/repository.py b/pre_commit/repository.py index fcc79bd6b..d2d30dfcc 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -36,7 +36,7 @@ def _state_filename(cmd_runner, venv): ) -def _read_installed_state(cmd_runner, venv): +def _read_state(cmd_runner, venv): filename = _state_filename(cmd_runner, venv) if not os.path.exists(filename): return None @@ -44,7 +44,7 @@ def _read_installed_state(cmd_runner, venv): return json.loads(io.open(filename).read()) -def _write_installed_state(cmd_runner, venv, state): +def _write_state(cmd_runner, venv, state): state_filename = _state_filename(cmd_runner, venv) staging = state_filename + 'staging' with io.open(staging, 'w') as state_file: @@ -57,8 +57,10 @@ def _installed(cmd_runner, language_name, language_version, additional_deps): language = languages[language_name] venv = environment_dir(language.ENVIRONMENT_DIR, language_version) return ( - venv is None or - _read_installed_state(cmd_runner, venv) == _state(additional_deps) + venv is None or ( + _read_state(cmd_runner, venv) == _state(additional_deps) and + language.healthy(cmd_runner, language_version) + ) ) @@ -89,7 +91,7 @@ def _install_all(venvs, repo_url): language.install_environment(cmd_runner, version, deps) # Write our state to indicate we're installed state = _state(deps) - _write_installed_state(cmd_runner, venv, state) + _write_state(cmd_runner, venv, state) def _validate_minimum_version(hook): diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index dd1ed27b2..95cec1044 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -40,3 +40,13 @@ def test_get_default_version_argspec(language): ) argspec = inspect.getargspec(languages[language].get_default_version) assert argspec == expected_argspec + + +@pytest.mark.parametrize('language', all_languages) +def test_healthy_argspec(language): + expected_argspec = inspect.ArgSpec( + args=['repo_cmd_runner', 'language_version'], + varargs=None, keywords=None, defaults=None, + ) + argspec = inspect.getargspec(languages[language].healthy) + assert argspec == expected_argspec diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py new file mode 100644 index 000000000..7019e260f --- /dev/null +++ b/tests/languages/helpers_test.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +from pre_commit.languages import helpers + + +def test_basic_get_default_version(): + assert helpers.basic_get_default_version() == 'default' + + +def test_basic_healthy(): + assert helpers.basic_healthy(None, None) is True diff --git a/tests/repository_test.py b/tests/repository_test.py index 9096161e9..80b4ccf74 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -600,6 +600,29 @@ class MyKeyboardInterrupt(KeyboardInterrupt): assert retv == 0 +def test_invalidated_virtualenv(tempdir_factory, store): + # A cached virtualenv may become invalidated if the system python upgrades + # This should not cause every hook in that virtualenv to fail. + path = make_repo(tempdir_factory, 'python_hooks_repo') + config = make_config_from_repo(path) + repo = Repository.create(config, store) + + # Simulate breaking of the virtualenv + repo.require_installed() + version = python.get_default_version() + libdir = repo._cmd_runner.path('py_env-{}'.format(version), 'lib', version) + paths = [ + os.path.join(libdir, p) for p in ('site.py', 'site.pyc', '__pycache__') + ] + cmd_output('rm', '-rf', *paths) + + # pre-commit should rebuild the virtualenv and it should be runnable + repo = Repository.create(config, store) + hook = repo.hooks[0][1] + retv, stdout, stderr = repo.run_hook(hook, []) + assert retv == 0 + + @pytest.mark.integration def test_really_long_file_paths(tempdir_factory, store): base_path = tempdir_factory.get() From dd182fb42e0820d25ca865249d8c86fe98a0c8ec Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 20 Jul 2017 16:00:14 -0700 Subject: [PATCH 016/544] v0.15.3 --- CHANGELOG.md | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 513671666..489f3b815 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +0.15.3 +====== +- Recover from invalid python virtualenvs + + 0.15.2 ====== - Work around a windows-specific virtualenv bug pypa/virtualenv#1062 diff --git a/setup.py b/setup.py index 424bfd6f4..26308704f 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.15.2', + version='0.15.3', author='Anthony Sottile', author_email='asottile@umich.edu', From a6a4762f0dc0591f6650d689951fd231eacc1ba7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 22 Jul 2017 15:57:37 -0700 Subject: [PATCH 017/544] Fix resetting of FakeStream --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 9813e9aca..3d97695a8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -170,7 +170,7 @@ def __init__(self, stream): def get_bytes(self): """Get the output as-if no encoding occurred""" data = self._stream.data.getvalue() - self._stream = io.BytesIO() + self._stream.data.truncate(0) return data def get(self): From d0b268c813cd31bf5af4da6288b85ddfaac029cb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 22 Jul 2017 15:16:12 -0700 Subject: [PATCH 018/544] Add support for commit-msg git hook --- pre_commit/commands/install_uninstall.py | 13 ++-- pre_commit/commands/run.py | 3 + pre_commit/main.py | 9 ++- pre_commit/resources/commit-msg-tmpl | 1 + pre_commit/resources/hook-tmpl | 2 +- testing/fixtures.py | 3 +- tests/commands/install_uninstall_test.py | 25 ++++++- tests/commands/run_test.py | 83 +++++++++++------------- tests/conftest.py | 25 +++++++ 9 files changed, 107 insertions(+), 57 deletions(-) create mode 100644 pre_commit/resources/commit-msg-tmpl diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 36b0d7d79..6e09dabde 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -56,16 +56,21 @@ def install( with io.open(hook_path, 'w') as pre_commit_file_obj: if hook_type == 'pre-push': - with io.open(resource_filename('pre-push-tmpl')) as fp: - pre_push_contents = fp.read() + with io.open(resource_filename('pre-push-tmpl')) as f: + hook_specific_contents = f.read() + elif hook_type == 'commit-msg': + with io.open(resource_filename('commit-msg-tmpl')) as f: + hook_specific_contents = f.read() + elif hook_type == 'pre-commit': + hook_specific_contents = '' else: - pre_push_contents = '' + raise AssertionError('Unknown hook type: {}'.format(hook_type)) skip_on_missing_conf = 'true' if skip_on_missing_conf else 'false' contents = io.open(resource_filename('hook-tmpl')).read().format( sys_executable=sys.executable, hook_type=hook_type, - pre_push=pre_push_contents, + hook_specific=hook_specific_contents, skip_on_missing_conf=skip_on_missing_conf, ) pre_commit_file_obj.write(contents) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 676c50442..c18f2aace 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -58,6 +58,9 @@ def get_filenames(args, include_expr, exclude_expr): getter = git.get_files_matching( lambda: get_changed_files(args.origin, args.source), ) + elif args.hook_stage == 'commit-msg': + def getter(*_): + return (args.commit_msg_filename,) elif args.files: getter = git.get_files_matching(lambda: args.files) elif args.all_files: diff --git a/pre_commit/main.py b/pre_commit/main.py index 02eed40e0..37fb264db 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -76,7 +76,7 @@ def main(argv=None): ), ) install_parser.add_argument( - '-t', '--hook-type', choices=('pre-commit', 'pre-push'), + '-t', '--hook-type', choices=('pre-commit', 'pre-push', 'commit-msg'), default='pre-commit', ) install_parser.add_argument( @@ -149,6 +149,10 @@ def main(argv=None): '--source', '-s', help="The remote branch's commit_id when using `git push`.", ) + run_parser.add_argument( + '--commit-msg-filename', + help='Filename to check when running during `commit-msg`', + ) run_parser.add_argument( '--allow-unstaged-config', default=False, action='store_true', help=( @@ -157,7 +161,8 @@ def main(argv=None): ), ) run_parser.add_argument( - '--hook-stage', choices=('commit', 'push'), default='commit', + '--hook-stage', choices=('commit', 'push', 'commit-msg'), + default='commit', help='The stage during which the hook is fired e.g. commit or push.', ) run_parser.add_argument( diff --git a/pre_commit/resources/commit-msg-tmpl b/pre_commit/resources/commit-msg-tmpl new file mode 100644 index 000000000..b11521b01 --- /dev/null +++ b/pre_commit/resources/commit-msg-tmpl @@ -0,0 +1 @@ +args="run --hook-stage=commit-msg --commit-msg-filename=$1" diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index da939ff1a..3bfce5c79 100644 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -52,7 +52,7 @@ if [ ! -f $CONF_FILE ]; then fi fi -{pre_push} +{hook_specific} # Run pre-commit if ((WHICH_RETV == 0)); then diff --git a/testing/fixtures.py b/testing/fixtures.py index be4342165..eda2e09ad 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -23,8 +23,7 @@ def git_dir(tempdir_factory): path = tempdir_factory.get() - with cwd(path): - cmd_output('git', 'init') + cmd_output('git', 'init', path) return path diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 1fb0f8f14..94d396a92 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -56,7 +56,7 @@ def test_install_pre_commit(tempdir_factory): expected_contents = io.open(pre_commit_script).read().format( sys_executable=sys.executable, hook_type='pre-commit', - pre_push='', + hook_specific='', skip_on_missing_conf='false', ) assert pre_commit_contents == expected_contents @@ -71,7 +71,7 @@ def test_install_pre_commit(tempdir_factory): expected_contents = io.open(pre_commit_script).read().format( sys_executable=sys.executable, hook_type='pre-push', - pre_push=pre_push_template_contents, + hook_specific=pre_push_template_contents, skip_on_missing_conf='false', ) assert pre_push_contents == expected_contents @@ -118,10 +118,11 @@ def test_uninstall(tempdir_factory): def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): + commit_msg = kwargs.pop('commit_msg', 'Commit!') open(touch_file, 'a').close() cmd_output('git', 'add', touch_file) return cmd_output_mocked_pre_commit_home( - 'git', 'commit', '-am', 'Commit!', '--allow-empty', + 'git', 'commit', '-am', commit_msg, '--allow-empty', # git commit puts pre-commit to stderr stderr=subprocess.STDOUT, retcode=None, @@ -560,6 +561,24 @@ def test_pre_push_integration_empty_push(tempdir_factory): assert retc == 0 +def test_commit_msg_integration_failing(commit_msg_repo, tempdir_factory): + install(Runner(commit_msg_repo, C.CONFIG_FILE), hook_type='commit-msg') + retc, out = _get_commit_output(tempdir_factory) + assert retc == 1 + assert out.startswith('Must have "Signed off by:"...') + assert out.strip().endswith('...Failed') + + +def test_commit_msg_integration_passing(commit_msg_repo, tempdir_factory): + install(Runner(commit_msg_repo, C.CONFIG_FILE), hook_type='commit-msg') + msg = 'Hi\nSigned off by: me, lol' + retc, out = _get_commit_output(tempdir_factory, commit_msg=msg) + assert retc == 0 + first_line = out.splitlines()[0] + assert first_line.startswith('Must have "Signed off by:"...') + assert first_line.endswith('...Passed') + + def test_install_disallow_mising_config(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 59f2ec9a6..c360fde94 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -60,6 +60,7 @@ def _get_opts( allow_unstaged_config=False, hook_stage='commit', show_diff_on_failure=False, + commit_msg_filename='', ): # These are mutually exclusive assert not (all_files and files) @@ -75,6 +76,7 @@ def _get_opts( allow_unstaged_config=allow_unstaged_config, hook_stage=hook_stage, show_diff_on_failure=show_diff_on_failure, + commit_msg_filename=commit_msg_filename, ) @@ -572,40 +574,7 @@ def test_lots_of_files(mock_out_store_directory, tempdir_factory): ) -@pytest.mark.parametrize( - ( - 'hook_stage', 'stage_for_first_hook', 'stage_for_second_hook', - 'expected_output', - ), - ( - ('push', ['commit'], ['commit'], [b'', b'']), - ( - 'push', ['commit', 'push'], ['commit', 'push'], - [b'hook 1', b'hook 2'], - ), - ('push', [], [], [b'hook 1', b'hook 2']), - ('push', [], ['commit'], [b'hook 1', b'']), - ('push', ['push'], ['commit'], [b'hook 1', b'']), - ('push', ['commit'], ['push'], [b'', b'hook 2']), - ( - 'commit', ['commit', 'push'], ['commit', 'push'], - [b'hook 1', b'hook 2'], - ), - ('commit', ['commit'], ['commit'], [b'hook 1', b'hook 2']), - ('commit', [], [], [b'hook 1', b'hook 2']), - ('commit', [], ['commit'], [b'hook 1', b'hook 2']), - ('commit', ['push'], ['commit'], [b'', b'hook 2']), - ('commit', ['commit'], ['push'], [b'hook 1', b'']), - ), -) -def test_local_hook_for_stages( - cap_out, - repo_with_passing_hook, mock_out_store_directory, - stage_for_first_hook, - stage_for_second_hook, - hook_stage, - expected_output, -): +def test_push_hook(cap_out, repo_with_passing_hook, mock_out_store_directory): config = OrderedDict(( ('repo', 'local'), ( @@ -613,37 +582,61 @@ def test_local_hook_for_stages( OrderedDict(( ('id', 'flake8'), ('name', 'hook 1'), - ('entry', 'python -m flake8.__main__'), + ('entry', "'{}' -m flake8".format(sys.executable)), ('language', 'system'), - ('files', r'\.py$'), - ('stages', stage_for_first_hook), - )), OrderedDict(( + ('types', ['python']), + ('stages', ['commit']), + )), + OrderedDict(( ('id', 'do_not_commit'), ('name', 'hook 2'), ('entry', 'DO NOT COMMIT'), ('language', 'pcre'), - ('files', '^(.*)$'), - ('stages', stage_for_second_hook), + ('types', ['text']), + ('stages', ['push']), )), ), ), )) add_config_to_repo(repo_with_passing_hook, config) - with io.open('dummy.py', 'w') as staged_file: - staged_file.write('"""TODO: something"""\n') + open('dummy.py', 'a').close() cmd_output('git', 'add', 'dummy.py') _test_run( cap_out, repo_with_passing_hook, - {'hook_stage': hook_stage}, - expected_outputs=expected_output, + {'hook_stage': 'commit'}, + expected_outputs=[b'hook 1'], + expected_ret=0, + stage=False, + ) + + _test_run( + cap_out, + repo_with_passing_hook, + {'hook_stage': 'push'}, + expected_outputs=[b'hook 2'], expected_ret=0, stage=False, ) +def test_commit_msg_hook(cap_out, commit_msg_repo, mock_out_store_directory): + filename = '.git/COMMIT_EDITMSG' + with io.open(filename, 'w') as f: + f.write('This is the commit message') + + _test_run( + cap_out, + commit_msg_repo, + {'hook_stage': 'commit-msg', 'commit_msg_filename': filename}, + expected_outputs=[b'Must have "Signed off by:"', b'Failed'], + expected_ret=1, + stage=False, + ) + + def test_local_hook_passes( cap_out, repo_with_passing_hook, mock_out_store_directory, ): @@ -654,7 +647,7 @@ def test_local_hook_passes( OrderedDict(( ('id', 'flake8'), ('name', 'flake8'), - ('entry', 'python -m flake8.__main__'), + ('entry', "'{}' -m flake8".format(sys.executable)), ('language', 'system'), ('files', r'\.py$'), )), OrderedDict(( diff --git a/tests/conftest.py b/tests/conftest.py index 3d97695a8..36743d885 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ from __future__ import absolute_import from __future__ import unicode_literals +import collections import functools import io import logging @@ -20,6 +21,7 @@ from pre_commit.util import cwd from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo +from testing.fixtures import write_config @pytest.yield_fixture @@ -92,6 +94,29 @@ def in_conflicting_submodule(tempdir_factory): yield +@pytest.fixture +def commit_msg_repo(tempdir_factory): + path = git_dir(tempdir_factory) + config = collections.OrderedDict(( + ('repo', 'local'), + ( + 'hooks', + [collections.OrderedDict(( + ('id', 'must-have-signoff'), + ('name', 'Must have "Signed off by:"'), + ('entry', 'grep -q "Signed off by:"'), + ('language', 'system'), + ('stages', ['commit-msg']), + ))], + ), + )) + write_config(path, config) + with cwd(path): + cmd_output('git', 'add', '.') + cmd_output('git', 'commit', '-m', 'add hooks') + yield path + + @pytest.yield_fixture(autouse=True, scope='session') def dont_write_to_home_directory(): """pre_commit.store.Store will by default write to the home directory From bbee21c98ef3dcb126ea697620434f518c30f481 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 23 Jul 2017 21:30:51 -0700 Subject: [PATCH 019/544] v0.15.4 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 489f3b815..d1b5c230f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.15.4 +====== +- Add support for the `commit-msg` git hook + 0.15.3 ====== - Recover from invalid python virtualenvs diff --git a/setup.py b/setup.py index 26308704f..020699c68 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.15.3', + version='0.15.4', author='Anthony Sottile', author_email='asottile@umich.edu', From 9640999fb084baae51e14537615b820eaa528e7e Mon Sep 17 00:00:00 2001 From: "Cimon Lucas (LCM)" Date: Sat, 29 Jul 2017 01:20:09 +0200 Subject: [PATCH 020/544] Making golang-based hooks compatible with Cygwin --- pre_commit/languages/golang.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index c091bacfd..4493d6161 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -2,6 +2,7 @@ import contextlib import os.path +import sys from pre_commit import git from pre_commit.envcontext import envcontext @@ -61,7 +62,12 @@ def install_environment(repo_cmd_runner, version, additional_dependencies): repo_cmd_runner, ('git', 'clone', '.', repo_src_dir), ) - env = dict(os.environ, GOPATH=directory) + if sys.platform == 'cygwin': # pragma: no cover + _, gopath, _ = cmd_output('cygpath', '-w', directory) + gopath = gopath.strip() + else: + gopath = directory + env = dict(os.environ, GOPATH=gopath) cmd_output('go', 'get', './...', cwd=repo_src_dir, env=env) for dependency in additional_dependencies: cmd_output('go', 'get', dependency, cwd=repo_src_dir, env=env) From 3fbe4f5a6ff5a4991ca9bfe0d42da14e1c6cd3dd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Jul 2017 16:20:45 -0400 Subject: [PATCH 021/544] Appease autopep8 --- pre_commit/languages/golang.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 4493d6161..87687234c 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -62,7 +62,7 @@ def install_environment(repo_cmd_runner, version, additional_dependencies): repo_cmd_runner, ('git', 'clone', '.', repo_src_dir), ) - if sys.platform == 'cygwin': # pragma: no cover + if sys.platform == 'cygwin': # pragma: no cover _, gopath, _ = cmd_output('cygpath', '-w', directory) gopath = gopath.strip() else: From 51ed907b9f15cf10d633499fae4dcb9c2dd205f8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 31 Jul 2017 10:37:19 -0700 Subject: [PATCH 022/544] Add more tests for staged-files-only with crlf diffs --- tests/staged_files_only_test.py | 56 +++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 5099c2d34..a00841aa5 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -312,3 +312,59 @@ def test_non_utf8_conflicting_diff(foo_staged, cmd_runner): with io.open('foo', 'w') as foo_file: foo_file.write('') _test_foo_state(foo_staged, contents, 'AM', encoding='latin-1') + + +@pytest.fixture +def in_git_dir(tmpdir): + with tmpdir.as_cwd(): + cmd_output('git', 'init', '.') + yield tmpdir + + +BEFORE = b'1\n2\n' +AFTER = b'3\n4\n' + + +def _crlf(b): + return b.replace(b'\n', b'\r\n') + + +def _write(b): + with open('foo', 'wb') as f: + f.write(b) + + +def git_add(): + cmd_output('git', 'add', 'foo') + + +def assert_no_diff(): + tree = cmd_output('git', 'write-tree')[1].strip() + cmd_output('git', 'diff-index', tree, '--exit-code') + + +BEFORE_AFTER = pytest.mark.parametrize( + ('before', 'after'), + ( + (BEFORE, AFTER), + (_crlf(BEFORE), _crlf(AFTER)), + (_crlf(BEFORE), AFTER), + (BEFORE, _crlf(AFTER)), + ), +) + + +@BEFORE_AFTER +def test_default(in_git_dir, cmd_runner, before, after): + _write(before) + git_add() + _write(after) + with staged_files_only(cmd_runner): + assert_no_diff() + + +@BEFORE_AFTER +@pytest.mark.parametrize('autocrlf', ('true', 'false', 'input')) +def test_autocrlf_true(in_git_dir, cmd_runner, before, after, autocrlf): + cmd_output('git', 'config', '--local', 'core.autocrlf', autocrlf) + test_default(in_git_dir, cmd_runner, before, after) From 5fa021058d68dea7d5ea1e409d1ebfc5db2d24d7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 31 Jul 2017 13:20:01 -0700 Subject: [PATCH 023/544] Simplify crlf tests --- tests/staged_files_only_test.py | 43 +++++++++------------------------ 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index a00841aa5..e066c27cd 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import io +import itertools import logging import os.path import shutil @@ -321,50 +322,30 @@ def in_git_dir(tmpdir): yield tmpdir -BEFORE = b'1\n2\n' -AFTER = b'3\n4\n' - - -def _crlf(b): - return b.replace(b'\n', b'\r\n') - - def _write(b): with open('foo', 'wb') as f: f.write(b) -def git_add(): - cmd_output('git', 'add', 'foo') - - def assert_no_diff(): tree = cmd_output('git', 'write-tree')[1].strip() cmd_output('git', 'diff-index', tree, '--exit-code') -BEFORE_AFTER = pytest.mark.parametrize( - ('before', 'after'), - ( - (BEFORE, AFTER), - (_crlf(BEFORE), _crlf(AFTER)), - (_crlf(BEFORE), AFTER), - (BEFORE, _crlf(AFTER)), - ), -) +bool_product = tuple(itertools.product((True, False), repeat=2)) + +@pytest.mark.parametrize(('crlf_before', 'crlf_after'), bool_product) +@pytest.mark.parametrize('autocrlf', ('true', 'false', 'input')) +def test_crlf(in_git_dir, cmd_runner, crlf_before, crlf_after, autocrlf): + cmd_output('git', 'config', '--local', 'core.autocrlf', autocrlf) + + before, after = b'1\n2\n', b'3\n4\n' + before = before.replace(b'\n', b'\r\n') if crlf_before else before + after = after.replace(b'\n', b'\r\n') if crlf_after else after -@BEFORE_AFTER -def test_default(in_git_dir, cmd_runner, before, after): _write(before) - git_add() + cmd_output('git', 'add', 'foo') _write(after) with staged_files_only(cmd_runner): assert_no_diff() - - -@BEFORE_AFTER -@pytest.mark.parametrize('autocrlf', ('true', 'false', 'input')) -def test_autocrlf_true(in_git_dir, cmd_runner, before, after, autocrlf): - cmd_output('git', 'config', '--local', 'core.autocrlf', autocrlf) - test_default(in_git_dir, cmd_runner, before, after) From d5e2af7de58a0478160785cc9b1100b1fcfc4687 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 31 Jul 2017 20:21:30 -0700 Subject: [PATCH 024/544] Fix patch applying when apply.whitespace=error --- pre_commit/staged_files_only.py | 10 ++++++++-- tests/staged_files_only_test.py | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 862c6bd1a..151e924a0 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -45,7 +45,10 @@ def staged_files_only(cmd_runner): finally: # Try to apply the patch we saved try: - cmd_runner.run(('git', 'apply', patch_filename), encoding=None) + cmd_runner.run( + ('git', 'apply', '--whitespace=nowarn', patch_filename), + encoding=None, + ) except CalledProcessError: logger.warning( 'Stashed changes conflicted with hook auto-fixes... ' @@ -55,7 +58,10 @@ def staged_files_only(cmd_runner): # by hooks. # Roll back the changes made by hooks. cmd_runner.run(['git', 'checkout', '--', '.']) - cmd_runner.run(('git', 'apply', patch_filename), encoding=None) + cmd_runner.run( + ('git', 'apply', patch_filename, '--whitespace=nowarn'), + encoding=None, + ) logger.info('Restored changes from {}.'.format(patch_filename)) else: # There weren't any staged files so we don't need to do anything diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index e066c27cd..ecaee814f 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -340,7 +340,7 @@ def assert_no_diff(): def test_crlf(in_git_dir, cmd_runner, crlf_before, crlf_after, autocrlf): cmd_output('git', 'config', '--local', 'core.autocrlf', autocrlf) - before, after = b'1\n2\n', b'3\n4\n' + before, after = b'1\n2\n', b'3\n4\n\n' before = before.replace(b'\n', b'\r\n') if crlf_before else before after = after.replace(b'\n', b'\r\n') if crlf_after else after @@ -349,3 +349,8 @@ def test_crlf(in_git_dir, cmd_runner, crlf_before, crlf_after, autocrlf): _write(after) with staged_files_only(cmd_runner): assert_no_diff() + + +def test_whitespace_errors(in_git_dir, cmd_runner): + cmd_output('git', 'config', '--local', 'apply.whitespace', 'error') + test_crlf(in_git_dir, cmd_runner, True, True, 'true') From ac0e1a60585ea903012bfadc2814874d8ab6369f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 31 Jul 2017 08:01:28 -0700 Subject: [PATCH 025/544] Use more git plumbing commands in staged-files-only --- pre_commit/staged_files_only.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 151e924a0..7db17b83d 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -20,10 +20,11 @@ def staged_files_only(cmd_runner): cmd_runner - PrefixedCommandRunner """ # Determine if there are unstaged files + tree = cmd_runner.run(('git', 'write-tree'))[1].strip() retcode, diff_stdout_binary, _ = cmd_runner.run( ( - 'git', 'diff', '--ignore-submodules', '--binary', '--exit-code', - '--no-color', '--no-ext-diff', + 'git', 'diff-index', '--ignore-submodules', '--binary', + '--exit-code', '--no-color', '--no-ext-diff', tree, '--', ), retcode=None, encoding=None, @@ -39,7 +40,7 @@ def staged_files_only(cmd_runner): patch_file.write(diff_stdout_binary) # Clear the working directory of unstaged changes - cmd_runner.run(['git', 'checkout', '--', '.']) + cmd_runner.run(('git', 'checkout', '--', '.')) try: yield finally: @@ -57,7 +58,7 @@ def staged_files_only(cmd_runner): # We failed to apply the patch, presumably due to fixes made # by hooks. # Roll back the changes made by hooks. - cmd_runner.run(['git', 'checkout', '--', '.']) + cmd_runner.run(('git', 'checkout', '--', '.')) cmd_runner.run( ('git', 'apply', patch_filename, '--whitespace=nowarn'), encoding=None, From 0548b0b521f49837c8c544976488f71c7393a56d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 1 Aug 2017 11:45:51 -0700 Subject: [PATCH 026/544] Workaround git apply with autocrlf=true bug --- pre_commit/staged_files_only.py | 20 ++++++++++++-------- tests/staged_files_only_test.py | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 7db17b83d..4d233924f 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -11,6 +11,16 @@ logger = logging.getLogger('pre_commit') +def _git_apply(cmd_runner, patch): + args = ('apply', '--whitespace=nowarn', patch) + try: + cmd_runner.run(('git',) + args, encoding=None) + except CalledProcessError: + # Retry with autocrlf=false -- see #570 + cmd = ('git', '-c', 'core.autocrlf=false') + args + cmd_runner.run(cmd, encoding=None) + + @contextlib.contextmanager def staged_files_only(cmd_runner): """Clear any unstaged changes from the git working directory inside this @@ -46,10 +56,7 @@ def staged_files_only(cmd_runner): finally: # Try to apply the patch we saved try: - cmd_runner.run( - ('git', 'apply', '--whitespace=nowarn', patch_filename), - encoding=None, - ) + _git_apply(cmd_runner, patch_filename) except CalledProcessError: logger.warning( 'Stashed changes conflicted with hook auto-fixes... ' @@ -59,10 +66,7 @@ def staged_files_only(cmd_runner): # by hooks. # Roll back the changes made by hooks. cmd_runner.run(('git', 'checkout', '--', '.')) - cmd_runner.run( - ('git', 'apply', patch_filename, '--whitespace=nowarn'), - encoding=None, - ) + _git_apply(cmd_runner, patch_filename) logger.info('Restored changes from {}.'.format(patch_filename)) else: # There weren't any staged files so we don't need to do anything diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index ecaee814f..78926d051 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -354,3 +354,17 @@ def test_crlf(in_git_dir, cmd_runner, crlf_before, crlf_after, autocrlf): def test_whitespace_errors(in_git_dir, cmd_runner): cmd_output('git', 'config', '--local', 'apply.whitespace', 'error') test_crlf(in_git_dir, cmd_runner, True, True, 'true') + + +def test_autocrlf_commited_crlf(in_git_dir, cmd_runner): + """Regression test for #570""" + cmd_output('git', 'config', '--local', 'core.autocrlf', 'false') + _write(b'1\r\n2\r\n') + cmd_output('git', 'add', 'foo') + cmd_output('git', 'commit', '-m', 'Check in crlf') + + cmd_output('git', 'config', '--local', 'core.autocrlf', 'true') + _write(b'1\r\n2\r\n\r\n\r\n\r\n') + + with staged_files_only(cmd_runner): + assert_no_diff() From a3f7b408abae0f170587c524e688be51cc944065 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 1 Aug 2017 13:41:54 -0700 Subject: [PATCH 027/544] v0.16.0 --- CHANGELOG.md | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1b5c230f..b9dcdca67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +0.16.0 +====== +- Remove backward compatibility with repositories providing metadata via + `hooks.yaml`. New repositories should provide `.pre-commit-hooks.yaml`. + Run `pre-commit autoupdate` to upgrade to the latest repositories. +- Improve golang support when running under cygwin. +- Fix crash with unstaged trailing whitespace additions while git was + configured with `apply.whitespace = error`. +- Fix crash with unstaged end-of-file crlf additions and the file's lines + ended with crlf while git was configured with `core-autocrlf = true`. + 0.15.4 ====== - Add support for the `commit-msg` git hook diff --git a/setup.py b/setup.py index 020699c68..88f92bfad 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.15.4', + version='0.16.0', author='Anthony Sottile', author_email='asottile@umich.edu', From b22ee6b1919be9418bfa06d3bac858f7973fdf37 Mon Sep 17 00:00:00 2001 From: "Cimon Lucas (LCM)" Date: Fri, 4 Aug 2017 10:48:21 +0200 Subject: [PATCH 028/544] NodeJS hooks compatibilty fix for Cygwin --- pre_commit/languages/node.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 69378b068..9110a3a9d 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -19,8 +19,8 @@ def get_env_patch(venv): # pragma: windows no cover return ( ('NODE_VIRTUAL_ENV', venv), - ('NPM_CONFIG_PREFIX', venv), - ('npm_config_prefix', venv), + ('NPM_CONFIG_PREFIX', os.path.join(venv, 'bin') if sys.platform == 'cygwin' else venv), + ('npm_config_prefix', os.path.join(venv, 'bin') if sys.platform == 'cygwin' else venv), ('NODE_PATH', os.path.join(venv, 'lib', 'node_modules')), ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), ) From 5f3e162646b0abb0a9cd5597acad3e549c430ef6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 4 Aug 2017 08:31:06 -0700 Subject: [PATCH 029/544] Appease flake8 --- pre_commit/languages/node.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 9110a3a9d..58922672b 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -17,10 +17,11 @@ def get_env_patch(venv): # pragma: windows no cover + config = os.path.join(venv, 'bin') if sys.platform == 'cygwin' else venv return ( ('NODE_VIRTUAL_ENV', venv), - ('NPM_CONFIG_PREFIX', os.path.join(venv, 'bin') if sys.platform == 'cygwin' else venv), - ('npm_config_prefix', os.path.join(venv, 'bin') if sys.platform == 'cygwin' else venv), + ('NPM_CONFIG_PREFIX', config), + ('npm_config_prefix', config), ('NODE_PATH', os.path.join(venv, 'lib', 'node_modules')), ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), ) From 677a4f607b793acd36bb038b15e668f44bc8716c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 4 Aug 2017 10:11:36 -0700 Subject: [PATCH 030/544] v0.16.1 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9dcdca67..7c8385b09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.16.1 +====== +- Improve node support when running under cygwin. + 0.16.0 ====== - Remove backward compatibility with repositories providing metadata via diff --git a/setup.py b/setup.py index 88f92bfad..f85450d88 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.16.0', + version='0.16.1', author='Anthony Sottile', author_email='asottile@umich.edu', From 8ad5536688b1dadf9b44d9df9a25ede9124a10e3 Mon Sep 17 00:00:00 2001 From: Martin von Gagern Date: Mon, 7 Aug 2017 03:52:06 +0100 Subject: [PATCH 031/544] Initialize git submodules Some packages make use of git submodules, and require all of them to be in place to be installed. One example is my libtidy wrapper for node, which depends on the C sources for libtidy (unless a precompiled binary is available for a given platform). I've been asked to add pre-commit support in https://github.com/gagern/node-libtidy/issues/17 but testing that failed because the source tree was lacking its submodules. --- pre_commit/store.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pre_commit/store.py b/pre_commit/store.py index ee1f755b9..84fc21233 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -123,6 +123,10 @@ def clone_strategy(directory): ) with cwd(directory): cmd_output('git', 'reset', ref, '--hard', env=no_git_env()) + cmd_output( + 'git', 'submodule', 'update', '--init', '--recursive', + env=no_git_env(), + ) return self._new_repo(repo, ref, clone_strategy) From fd01f2d8bf07acc7bb4c511df65e785a61ce1724 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 6 Aug 2017 20:01:36 -0700 Subject: [PATCH 032/544] Update swift urls (the old ones 404d) --- get-swift.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/get-swift.sh b/get-swift.sh index e5cc570b3..667ef4c80 100755 --- a/get-swift.sh +++ b/get-swift.sh @@ -4,9 +4,9 @@ set -ex . /etc/lsb-release if [ "$DISTRIB_CODENAME" = "trusty" ]; then - SWIFT_URL='https://swift.org/builds/swift-3.0.2-release/ubuntu1404/swift-3.0.2-RELEASE/swift-3.0.2-RELEASE-ubuntu14.04.tar.gz' + SWIFT_URL='https://swift.org/builds/swift-3.1.1-release/ubuntu1404/swift-3.1.1-RELEASE/swift-3.1.1-RELEASE-ubuntu14.04.tar.gz' else - SWIFT_URL='https://swift.org/builds/swift-3.0.2-release/ubuntu1604/swift-3.0.2-RELEASE/swift-3.0.2-RELEASE-ubuntu16.04.tar.gz' + SWIFT_URL='https://swift.org/builds/swift-3.1.1-release/ubuntu1604/swift-3.1.1-RELEASE/swift-3.1.1-RELEASE-ubuntu16.04.tar.gz' fi mkdir -p /tmp/swift From 72efbb3950ef5943156c0171737b3d7edcfa00e7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 6 Aug 2017 20:24:15 -0700 Subject: [PATCH 033/544] v0.16.2 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c8385b09..758171102 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.16.2 +====== +- Initialize submodules in hook repositories. + 0.16.1 ====== - Improve node support when running under cygwin. diff --git a/setup.py b/setup.py index f85450d88..3052abd1c 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.16.1', + version='0.16.2', author='Anthony Sottile', author_email='asottile@umich.edu', From 59c6df5e460185dbe1deeb6790076e30e97150bb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 10 Aug 2017 17:57:53 -0700 Subject: [PATCH 034/544] When possible, preserve config format on autoupdate --- pre_commit/commands/autoupdate.py | 45 ++++++++++++++++++++++--- tests/commands/autoupdate_test.py | 56 +++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 5 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 620a8a6e6..69ff2782f 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -1,6 +1,7 @@ from __future__ import print_function from __future__ import unicode_literals +import re from collections import OrderedDict from aspy.yaml import ordered_dump @@ -65,6 +66,44 @@ def _update_repo(repo_config, runner, tags_only): return new_config +SHA_LINE_RE = re.compile(r'^(\s+)sha:(\s*)([^\s#]+)(.*)$', re.DOTALL) +SHA_LINE_FMT = '{}sha:{}{}{}' + + +def _write_new_config_file(path, output): + original_contents = open(path).read() + output = remove_defaults(output, CONFIG_SCHEMA) + new_contents = ordered_dump(output, **C.YAML_DUMP_KWARGS) + + lines = original_contents.splitlines(True) + sha_line_indices_rev = list(reversed([ + i for i, line in enumerate(lines) if SHA_LINE_RE.match(line) + ])) + + for line in new_contents.splitlines(True): + if SHA_LINE_RE.match(line): + # It's possible we didn't identify the sha lines in the original + if not sha_line_indices_rev: + break + line_index = sha_line_indices_rev.pop() + original_line = lines[line_index] + orig_match = SHA_LINE_RE.match(original_line) + new_match = SHA_LINE_RE.match(line) + lines[line_index] = SHA_LINE_FMT.format( + orig_match.group(1), orig_match.group(2), + new_match.group(3), orig_match.group(4), + ) + + # If we failed to intelligently rewrite the sha lines, fall back to the + # pretty-formatted yaml output + to_write = ''.join(lines) + if ordered_load(to_write) != output: + to_write = new_contents + + with open(path, 'w') as f: + f.write(to_write) + + def autoupdate(runner, tags_only): """Auto-update the pre-commit config to the latest versions of repos.""" retv = 0 @@ -100,10 +139,6 @@ def autoupdate(runner, tags_only): output_configs.append(repo_config) if changed: - with open(runner.config_file_path, 'w') as config_file: - config_file.write(ordered_dump( - remove_defaults(output_configs, CONFIG_SCHEMA), - **C.YAML_DUMP_KWARGS - )) + _write_new_config_file(runner.config_file_path, output_configs) return retv diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 8dac48c4c..1920610a1 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import pipes import shutil from collections import OrderedDict @@ -123,6 +124,61 @@ def test_autoupdate_out_of_date_repo( assert out_of_date_repo.head_sha in after +def test_does_not_reformat( + out_of_date_repo, mock_out_store_directory, in_tmpdir, +): + fmt = ( + '- repo: {}\n' + ' sha: {} # definitely the version I want!\n' + ' hooks:\n' + ' - id: foo\n' + ' # These args are because reasons!\n' + ' args: [foo, bar, baz]\n' + ) + config = fmt.format(out_of_date_repo.path, out_of_date_repo.original_sha) + with open(C.CONFIG_FILE, 'w') as f: + f.write(config) + + autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) + after = open(C.CONFIG_FILE).read() + expected = fmt.format(out_of_date_repo.path, out_of_date_repo.head_sha) + assert after == expected + + +def test_loses_formatting_when_not_detectable( + out_of_date_repo, mock_out_store_directory, in_tmpdir, +): + """A best-effort attempt is made at updating sha without rewriting + formatting. When the original formatting cannot be detected, this + is abandoned. + """ + config = ( + '[\n' + ' {{\n' + ' repo: {}, sha: {},\n' + ' hooks: [\n' + ' # A comment!\n' + ' {{id: foo}},\n' + ' ],\n' + ' }}\n' + ']\n'.format( + pipes.quote(out_of_date_repo.path), out_of_date_repo.original_sha, + ) + ) + with open(C.CONFIG_FILE, 'w') as f: + f.write(config) + + autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) + after = open(C.CONFIG_FILE).read() + expected = ( + '- repo: {}\n' + ' sha: {}\n' + ' hooks:\n' + ' - id: foo\n' + ).format(out_of_date_repo.path, out_of_date_repo.head_sha) + assert after == expected + + @pytest.yield_fixture def tagged_repo(out_of_date_repo): with cwd(out_of_date_repo.path): From ee392275f308032dc47ec0dea9d19c92b89d5996 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 10 Aug 2017 19:09:25 -0700 Subject: [PATCH 035/544] Remove remove_defaults -- it wasn't doing anything --- pre_commit/commands/autoupdate.py | 3 --- pre_commit/schema.py | 27 ---------------------- tests/schema_test.py | 38 ------------------------------- 3 files changed, 68 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 69ff2782f..36df87f8c 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -9,11 +9,9 @@ import pre_commit.constants as C from pre_commit import output -from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import is_local_repo from pre_commit.clientlib import load_config from pre_commit.repository import Repository -from pre_commit.schema import remove_defaults from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cwd @@ -72,7 +70,6 @@ def _update_repo(repo_config, runner, tags_only): def _write_new_config_file(path, output): original_contents = open(path).read() - output = remove_defaults(output, CONFIG_SCHEMA) new_contents = ordered_dump(output, **C.YAML_DUMP_KWARGS) lines = original_contents.splitlines(True) diff --git a/pre_commit/schema.py b/pre_commit/schema.py index 5f22277d8..a911bb437 100644 --- a/pre_commit/schema.py +++ b/pre_commit/schema.py @@ -64,11 +64,6 @@ def _apply_default_optional(self, dct): dct.setdefault(self.key, self.default) -def _remove_default_optional(self, dct): - if dct.get(self.key, MISSING) == self.default: - del dct[self.key] - - def _require_key(self, dct): if self.key not in dct: raise ValidationError('Missing required key: {}'.format(self.key)) @@ -90,10 +85,6 @@ def _apply_default_required_recurse(self, dct): dct[self.key] = apply_defaults(dct[self.key], self.schema) -def _remove_default_required_recurse(self, dct): - dct[self.key] = remove_defaults(dct[self.key], self.schema) - - def _check_conditional(self, dct): if dct.get(self.condition_key, MISSING) == self.condition_value: _check_required(self, dct) @@ -119,22 +110,18 @@ def _check_conditional(self, dct): Required = collections.namedtuple('Required', ('key', 'check_fn')) Required.check = _check_required Required.apply_default = _dct_noop -Required.remove_default = _dct_noop RequiredRecurse = collections.namedtuple('RequiredRecurse', ('key', 'schema')) RequiredRecurse.check = _check_required RequiredRecurse.check_fn = _check_fn_required_recurse RequiredRecurse.apply_default = _apply_default_required_recurse -RequiredRecurse.remove_default = _remove_default_required_recurse Optional = collections.namedtuple('Optional', ('key', 'check_fn', 'default')) Optional.check = _check_optional Optional.apply_default = _apply_default_optional -Optional.remove_default = _remove_default_optional OptionalNoDefault = collections.namedtuple( 'OptionalNoDefault', ('key', 'check_fn'), ) OptionalNoDefault.check = _check_optional OptionalNoDefault.apply_default = _dct_noop -OptionalNoDefault.remove_default = _dct_noop Conditional = collections.namedtuple( 'Conditional', ('key', 'check_fn', 'condition_key', 'condition_value', 'ensure_absent'), @@ -142,7 +129,6 @@ def _check_conditional(self, dct): Conditional.__new__.__defaults__ = (False,) Conditional.check = _check_conditional Conditional.apply_default = _dct_noop -Conditional.remove_default = _dct_noop class Map(collections.namedtuple('Map', ('object_name', 'id_key', 'items'))): @@ -168,12 +154,6 @@ def apply_defaults(self, v): item.apply_default(ret) return ret - def remove_defaults(self, v): - ret = v.copy() - for item in self.items: - item.remove_default(ret) - return ret - class Array(collections.namedtuple('Array', ('of',))): __slots__ = () @@ -190,9 +170,6 @@ def check(self, v): def apply_defaults(self, v): return [apply_defaults(val, self.of) for val in v] - def remove_defaults(self, v): - return [remove_defaults(val, self.of) for val in v] - class Not(object): def __init__(self, val): @@ -257,10 +234,6 @@ def apply_defaults(v, schema): return schema.apply_defaults(v) -def remove_defaults(v, schema): - return schema.remove_defaults(v) - - def load_from_filename(filename, schema, load_strategy, exc_tp): with reraise_as(exc_tp): if not os.path.exists(filename): diff --git a/tests/schema_test.py b/tests/schema_test.py index c2ecf0faa..c133a9971 100644 --- a/tests/schema_test.py +++ b/tests/schema_test.py @@ -21,7 +21,6 @@ from pre_commit.schema import Not from pre_commit.schema import Optional from pre_commit.schema import OptionalNoDefault -from pre_commit.schema import remove_defaults from pre_commit.schema import Required from pre_commit.schema import RequiredRecurse from pre_commit.schema import validate @@ -281,37 +280,6 @@ def test_apply_defaults_map_in_list(): assert ret == [{'key': False}] -def test_remove_defaults_copies_object(): - val = {'key': False} - ret = remove_defaults(val, map_optional) - assert ret is not val - - -def test_remove_defaults_removes_defaults(): - ret = remove_defaults({'key': False}, map_optional) - assert ret == {} - - -def test_remove_defaults_nothing_to_remove(): - ret = remove_defaults({}, map_optional) - assert ret == {} - - -def test_remove_defaults_does_not_change_non_default(): - ret = remove_defaults({'key': True}, map_optional) - assert ret == {'key': True} - - -def test_remove_defaults_map_in_list(): - ret = remove_defaults([{'key': False}], Array(map_optional)) - assert ret == [{}] - - -def test_remove_defaults_does_nothing_on_non_optional(): - ret = remove_defaults({'key': True}, map_required) - assert ret == {'key': True} - - nested_schema_required = Map( 'Repository', 'repo', Required('repo', check_any), @@ -342,12 +310,6 @@ def test_apply_defaults_nested(): assert ret == {'repo': 'repo1', 'hooks': [{'key': False}]} -def test_remove_defaults_nested(): - val = {'repo': 'repo1', 'hooks': [{'key': False}]} - ret = remove_defaults(val, nested_schema_optional) - assert ret == {'repo': 'repo1', 'hooks': [{}]} - - class Error(Exception): pass From 49366f1c4ac04d7ec0279707368e8611dcd950ed Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 10 Aug 2017 19:59:53 -0700 Subject: [PATCH 036/544] v0.16.3 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 758171102..86e5049ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.16.3 +====== +- autoupdate attempts to maintain config formatting. + 0.16.2 ====== - Initialize submodules in hook repositories. diff --git a/setup.py b/setup.py index 3052abd1c..1bb2651fd 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.16.2', + version='0.16.3', author='Anthony Sottile', author_email='asottile@umich.edu', From e90778222dc143d2ad6a00fbcc2bb1ac48d36a7f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 10 Aug 2017 20:18:37 -0700 Subject: [PATCH 037/544] Fix a typo in the install help --- pre_commit/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 37fb264db..21e927401 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -82,7 +82,7 @@ def main(argv=None): install_parser.add_argument( '--allow-missing-config', action='store_true', default=False, help=( - 'Whether to allow a missing `pre-config` configuration file ' + 'Whether to allow a missing `pre-commit` configuration file ' 'or exit with a failure code.' ), ) From 5491f8b5eb6b0461c9ca900f4481c4da2c392097 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 10 Aug 2017 20:21:09 -0700 Subject: [PATCH 038/544] Allow commit-msg hooks to be uninstalled --- pre_commit/main.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 37fb264db..9daeb980a 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -45,6 +45,13 @@ def _add_config_option(parser): ) +def _add_hook_type_option(parser): + parser.add_argument( + '-t', '--hook-type', choices=('pre-commit', 'pre-push', 'commit-msg'), + default='pre-commit', + ) + + def main(argv=None): argv = argv if argv is not None else sys.argv[1:] argv = [five.to_text(arg) for arg in argv] @@ -75,10 +82,7 @@ def main(argv=None): 'in the config file.' ), ) - install_parser.add_argument( - '-t', '--hook-type', choices=('pre-commit', 'pre-push', 'commit-msg'), - default='pre-commit', - ) + _add_hook_type_option(install_parser) install_parser.add_argument( '--allow-missing-config', action='store_true', default=False, help=( @@ -103,10 +107,7 @@ def main(argv=None): ) _add_color_option(uninstall_parser) _add_config_option(uninstall_parser) - uninstall_parser.add_argument( - '-t', '--hook-type', choices=('pre-commit', 'pre-push'), - default='pre-commit', - ) + _add_hook_type_option(uninstall_parser) clean_parser = subparsers.add_parser( 'clean', help='Clean out pre-commit files.', From 469498ac9df818bba3f5ca9978e12ad47d1780c6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 10 Aug 2017 20:24:24 -0700 Subject: [PATCH 039/544] Upgrade the sample-config output --- pre_commit/commands/sample_config.py | 2 +- tests/commands/sample_config_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/sample_config.py b/pre_commit/commands/sample_config.py index f38d655f0..b74e4271c 100644 --- a/pre_commit/commands/sample_config.py +++ b/pre_commit/commands/sample_config.py @@ -11,7 +11,7 @@ # See http://pre-commit.com for more information # See http://pre-commit.com/hooks.html for more hooks - repo: https://github.com/pre-commit/pre-commit-hooks - sha: v0.7.1 + sha: v0.9.1 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/tests/commands/sample_config_test.py b/tests/commands/sample_config_test.py index 88a90d914..122d7bfc5 100644 --- a/tests/commands/sample_config_test.py +++ b/tests/commands/sample_config_test.py @@ -12,7 +12,7 @@ def test_sample_config(capsys): # See http://pre-commit.com for more information # See http://pre-commit.com/hooks.html for more hooks - repo: https://github.com/pre-commit/pre-commit-hooks - sha: v0.7.1 + sha: v0.9.1 hooks: - id: trailing-whitespace - id: end-of-file-fixer From 2694ebe26478fb2da6d5f778ea7cf3f523f62c05 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 14 Aug 2017 18:28:55 -0700 Subject: [PATCH 040/544] Ran pre-commit autoupdate. Committed via https://github.com/asottile/all-repos --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index af8f1a7ce..24945961b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ - repo: https://github.com/pre-commit/pre-commit-hooks.git - sha: v0.9.0 + sha: v0.9.1 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -12,16 +12,16 @@ - id: requirements-txt-fixer - id: flake8 - repo: https://github.com/pre-commit/pre-commit.git - sha: v0.15.0 + sha: v0.16.3 hooks: - id: validate_config - id: validate_manifest - repo: https://github.com/asottile/reorder_python_imports.git - sha: v0.3.4 + sha: v0.3.5 hooks: - id: reorder-python-imports language_version: python2.7 - repo: https://github.com/asottile/add-trailing-comma - sha: v0.6.1 + sha: v0.6.4 hooks: - id: add-trailing-comma From 6793fd8e5d2363886650780a48e2b1232c4dc62a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 23 Aug 2017 10:24:02 -0700 Subject: [PATCH 041/544] Remove --no-stash and --allow-unstaged-config --- pre_commit/commands/run.py | 21 ++++--------- pre_commit/main.py | 13 +------- tests/commands/run_test.py | 62 +++----------------------------------- tests/git_test.py | 3 +- 4 files changed, 12 insertions(+), 87 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index c18f2aace..55d2b12ff 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -217,7 +217,7 @@ def _has_unstaged_config(runner): def run(runner, args, environ=os.environ): - no_stash = args.no_stash or args.all_files or bool(args.files) + no_stash = args.all_files or bool(args.files) # Check if we have unresolved merge conflict files and fail fast. if _has_unmerged_paths(runner): @@ -227,20 +227,11 @@ def run(runner, args, environ=os.environ): logger.error('Specify both --origin and --source.') return 1 if _has_unstaged_config(runner) and not no_stash: - if args.allow_unstaged_config: - logger.warn( - 'You have an unstaged config file and have specified the ' - '--allow-unstaged-config option.\n' - 'Note that your config will be stashed before the config is ' - 'parsed unless --no-stash is specified.', - ) - else: - logger.error( - 'Your .pre-commit-config.yaml is unstaged.\n' - '`git add .pre-commit-config.yaml` to fix this.\n' - 'Run pre-commit with --allow-unstaged-config to silence this.', - ) - return 1 + logger.error( + 'Your .pre-commit-config.yaml is unstaged.\n' + '`git add .pre-commit-config.yaml` to fix this.\n', + ) + return 1 # Expose origin / source as environment variables for hooks to consume if args.origin and args.source: diff --git a/pre_commit/main.py b/pre_commit/main.py index 3a2fee151..0b00a86ee 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -135,10 +135,6 @@ def main(argv=None): _add_color_option(run_parser) _add_config_option(run_parser) run_parser.add_argument('hook', nargs='?', help='A single hook-id to run') - run_parser.add_argument( - '--no-stash', default=False, action='store_true', - help='Use this option to prevent auto stashing of unstaged files.', - ) run_parser.add_argument( '--verbose', '-v', action='store_true', default=False, ) @@ -154,13 +150,6 @@ def main(argv=None): '--commit-msg-filename', help='Filename to check when running during `commit-msg`', ) - run_parser.add_argument( - '--allow-unstaged-config', default=False, action='store_true', - help=( - 'Allow an unstaged config to be present. Note that this will ' - 'be stashed before parsing unless --no-stash is specified.' - ), - ) run_parser.add_argument( '--hook-stage', choices=('commit', 'push', 'commit-msg'), default='commit', @@ -173,7 +162,7 @@ def main(argv=None): run_mutex_group = run_parser.add_mutually_exclusive_group(required=False) run_mutex_group.add_argument( '--all-files', '-a', action='store_true', default=False, - help='Run on all the files in the repo. Implies --no-stash.', + help='Run on all the files in the repo.', ) run_mutex_group.add_argument( '--files', nargs='*', default=[], diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index c360fde94..924d097fc 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -54,10 +54,8 @@ def _get_opts( color=False, verbose=False, hook=None, - no_stash=False, origin='', source='', - allow_unstaged_config=False, hook_stage='commit', show_diff_on_failure=False, commit_msg_filename='', @@ -70,10 +68,8 @@ def _get_opts( color=color, verbose=verbose, hook=hook, - no_stash=no_stash, origin=origin, source=source, - allow_unstaged_config=allow_unstaged_config, hook_stage=hook_stage, show_diff_on_failure=show_diff_on_failure, commit_msg_filename=commit_msg_filename, @@ -332,38 +328,6 @@ def test_origin_source_error_msg( assert warning_msg not in printed -@pytest.mark.parametrize( - ('no_stash', 'all_files', 'expect_stash'), - ( - (True, True, False), - (True, False, False), - (False, True, False), - (False, False, True), - ), -) -def test_no_stash( - cap_out, - repo_with_passing_hook, - no_stash, - all_files, - expect_stash, - mock_out_store_directory, -): - stage_a_file() - # Make unstaged changes - with open('foo.py', 'w') as foo_file: - foo_file.write('import os\n') - - args = _get_opts(no_stash=no_stash, all_files=all_files) - ret, printed = _do_run(cap_out, repo_with_passing_hook, args) - assert ret == 0 - warning_msg = b'[WARNING] Unstaged files detected.' - if expect_stash: - assert warning_msg in printed - else: - assert warning_msg not in printed - - @pytest.mark.parametrize(('output', 'expected'), (('some', True), ('', False))) def test_has_unmerged_paths(output, expected): mock_runner = mock.Mock() @@ -715,37 +679,19 @@ def modified_config_repo(repo_with_passing_hook): yield repo_with_passing_hook -def test_allow_unstaged_config_option( +def test_error_with_unstaged_config( cap_out, modified_config_repo, mock_out_store_directory, ): - args = _get_opts(allow_unstaged_config=True) - ret, printed = _do_run(cap_out, modified_config_repo, args) - expected = ( - b'You have an unstaged config file and have specified the ' - b'--allow-unstaged-config option.' - ) - assert expected in printed - assert ret == 0 - - -def test_no_allow_unstaged_config_option( - cap_out, modified_config_repo, mock_out_store_directory, -): - args = _get_opts(allow_unstaged_config=False) + args = _get_opts() ret, printed = _do_run(cap_out, modified_config_repo, args) assert b'Your .pre-commit-config.yaml is unstaged.' in printed assert ret == 1 @pytest.mark.parametrize( - 'opts', - ( - {'allow_unstaged_config': False, 'no_stash': True}, - {'all_files': True}, - {'files': [C.CONFIG_FILE]}, - ), + 'opts', ({'all_files': True}, {'files': [C.CONFIG_FILE]}), ) -def test_unstaged_message_suppressed( +def test_no_unstaged_error_with_all_files_or_files( cap_out, modified_config_repo, mock_out_store_directory, opts, ): args = _get_opts(**opts) diff --git a/tests/git_test.py b/tests/git_test.py index 4ffccee3a..0500a42df 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -137,8 +137,7 @@ def test_get_conflicted_files_in_submodule(in_conflicting_submodule): def test_get_conflicted_files_unstaged_files(in_merge_conflict): - # If they for whatever reason did pre-commit run --no-stash during a - # conflict + """This case no longer occurs, but it is a useful test nonetheless""" resolve_conflict() # Make unstaged file. From dc5a8a8209d71909a7d2c1d0abdceec888a48689 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 23 Aug 2017 14:06:48 -0700 Subject: [PATCH 042/544] Remove validate_config hook --- .pre-commit-config.yaml | 1 - .pre-commit-hooks.yaml | 6 ------ 2 files changed, 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 24945961b..1e529a7f2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,6 @@ - repo: https://github.com/pre-commit/pre-commit.git sha: v0.16.3 hooks: - - id: validate_config - id: validate_manifest - repo: https://github.com/asottile/reorder_python_imports.git sha: v0.3.5 diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index af53043ed..ef269d133 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -1,9 +1,3 @@ -- id: validate_config - name: Validate Pre-Commit Config - description: This validator validates a pre-commit hooks config file - entry: pre-commit-validate-config - language: python - files: ^\.pre-commit-config\.yaml$ - id: validate_manifest name: Validate Pre-Commit Manifest description: This validator validates a pre-commit hooks manifest file From 9a579b580d5ae0c178bdb6ef26c21218ac499703 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 23 Aug 2017 14:20:33 -0700 Subject: [PATCH 043/544] Remove extra newline on error() call --- pre_commit/commands/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 55d2b12ff..ac418a787 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -229,7 +229,7 @@ def run(runner, args, environ=os.environ): if _has_unstaged_config(runner) and not no_stash: logger.error( 'Your .pre-commit-config.yaml is unstaged.\n' - '`git add .pre-commit-config.yaml` to fix this.\n', + '`git add .pre-commit-config.yaml` to fix this.', ) return 1 From bba711f4689be1375e79b7cf624a2a7192671eca Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 23 Aug 2017 14:20:33 -0700 Subject: [PATCH 044/544] Remove extra newline on error() call --- pre_commit/commands/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 55d2b12ff..ac418a787 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -229,7 +229,7 @@ def run(runner, args, environ=os.environ): if _has_unstaged_config(runner) and not no_stash: logger.error( 'Your .pre-commit-config.yaml is unstaged.\n' - '`git add .pre-commit-config.yaml` to fix this.\n', + '`git add .pre-commit-config.yaml` to fix this.', ) return 1 From 625aaf54aa7ee28ec78fcaf495152c154ceede91 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 23 Aug 2017 17:24:49 -0700 Subject: [PATCH 045/544] Limit repository creation to one process --- pre_commit/file_lock.py | 53 +++++++++++++++++ pre_commit/repository.py | 46 +++++++++------ pre_commit/store.py | 73 ++++++++++++++++-------- tests/commands/install_uninstall_test.py | 7 ++- tests/repository_test.py | 4 +- tests/store_test.py | 2 +- 6 files changed, 136 insertions(+), 49 deletions(-) create mode 100644 pre_commit/file_lock.py diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py new file mode 100644 index 000000000..054ac529b --- /dev/null +++ b/pre_commit/file_lock.py @@ -0,0 +1,53 @@ +import contextlib +import errno + + +try: # pragma: no cover (windows) + import msvcrt + + # https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/locking + + # on windows we lock "regions" of files, we don't care about the actual + # byte region so we'll just pick *some* number here. + _region = 0xffff + + @contextlib.contextmanager + def _locked(fileno): + while True: + try: + msvcrt.locking(fileno, msvcrt.LK_LOCK, _region) + except OSError as e: + # Locking violation. Returned when the _LK_LOCK or _LK_RLCK + # flag is specified and the file cannot be locked after 10 + # attempts. + if e.errno != errno.EDEADLOCK: + raise + else: + break + + try: + yield + finally: + # From cursory testing, it seems to get unlocked when the file is + # closed so this may not be necessary. + # The documentation however states: + # "Regions should be locked only briefly and should be unlocked + # before closing a file or exiting the program." + msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region) +except ImportError: # pragma: no cover (posix) + import fcntl + + @contextlib.contextmanager + def _locked(fileno): + fcntl.flock(fileno, fcntl.LOCK_EX) + try: + yield + finally: + fcntl.flock(fileno, fcntl.LOCK_UN) + + +@contextlib.contextmanager +def lock(path): + with open(path, 'a+') as f: + with _locked(f.fileno()): + yield diff --git a/pre_commit/repository.py b/pre_commit/repository.py index d2d30dfcc..18f902cb1 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -64,34 +64,42 @@ def _installed(cmd_runner, language_name, language_version, additional_deps): ) -def _install_all(venvs, repo_url): +def _install_all(venvs, repo_url, store): """Tuple of (cmd_runner, language, version, deps)""" - need_installed = tuple( - (cmd_runner, language_name, version, deps) - for cmd_runner, language_name, version, deps in venvs - if not _installed(cmd_runner, language_name, version, deps) - ) + def _need_installed(): + return tuple( + (cmd_runner, language_name, version, deps) + for cmd_runner, language_name, version, deps in venvs + if not _installed(cmd_runner, language_name, version, deps) + ) + + if not _need_installed(): + return + with store.exclusive_lock(): + # Another process may have already completed this work + need_installed = _need_installed() + if not need_installed: # pragma: no cover (race) + return - if need_installed: logger.info( 'Installing environment for {}.'.format(repo_url), ) logger.info('Once installed this environment will be reused.') logger.info('This may take a few minutes...') - for cmd_runner, language_name, version, deps in need_installed: - language = languages[language_name] - venv = environment_dir(language.ENVIRONMENT_DIR, version) + for cmd_runner, language_name, version, deps in need_installed: + language = languages[language_name] + venv = environment_dir(language.ENVIRONMENT_DIR, version) - # There's potentially incomplete cleanup from previous runs - # Clean it up! - if cmd_runner.exists(venv): - shutil.rmtree(cmd_runner.path(venv)) + # There's potentially incomplete cleanup from previous runs + # Clean it up! + if cmd_runner.exists(venv): + shutil.rmtree(cmd_runner.path(venv)) - language.install_environment(cmd_runner, version, deps) - # Write our state to indicate we're installed - state = _state(deps) - _write_state(cmd_runner, venv, state) + language.install_environment(cmd_runner, version, deps) + # Write our state to indicate we're installed + state = _state(deps) + _write_state(cmd_runner, venv, state) def _validate_minimum_version(hook): @@ -174,7 +182,7 @@ def _venvs(self): def require_installed(self): if not self.__installed: - _install_all(self._venvs, self.repo_config['repo']) + _install_all(self._venvs, self.repo_config['repo'], self.store) self.__installed = True def run_hook(self, hook, file_args): diff --git a/pre_commit/store.py b/pre_commit/store.py index 84fc21233..29237870c 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -10,6 +10,7 @@ from cached_property import cached_property import pre_commit.constants as C +from pre_commit import file_lock from pre_commit.prefixed_command_runner import PrefixedCommandRunner from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output @@ -37,13 +38,20 @@ def _get_default_directory(): class Store(object): get_default_directory = staticmethod(_get_default_directory) + __created = False def __init__(self, directory=None): if directory is None: directory = self.get_default_directory() self.directory = directory - self.__created = False + + @contextlib.contextmanager + def exclusive_lock(self, quiet=False): + if not quiet: + logger.info('Locking pre-commit directory') + with file_lock.lock(os.path.join(self.directory, '.lock')): + yield def _write_readme(self): with io.open(os.path.join(self.directory, 'README'), 'w') as readme: @@ -75,12 +83,17 @@ def _write_sqlite_db(self): os.rename(tmpfile, self.db_path) def _create(self): - if os.path.exists(self.db_path): - return if not os.path.exists(self.directory): os.makedirs(self.directory) self._write_readme() - self._write_sqlite_db() + + if os.path.exists(self.db_path): + return + with self.exclusive_lock(quiet=True): + # Another process may have already completed this work + if os.path.exists(self.db_path): # pragma: no cover (race) + return + self._write_sqlite_db() def require_created(self): """Require the pre-commit file store to be created.""" @@ -91,27 +104,37 @@ def require_created(self): def _new_repo(self, repo, ref, make_strategy): self.require_created() - # Check if we already exist - with sqlite3.connect(self.db_path) as db: - result = db.execute( - 'SELECT path FROM repos WHERE repo = ? AND ref = ?', - [repo, ref], - ).fetchone() - if result: - return result[0] - - logger.info('Initializing environment for {}.'.format(repo)) - - directory = tempfile.mkdtemp(prefix='repo', dir=self.directory) - with clean_path_on_failure(directory): - make_strategy(directory) - - # Update our db with the created repo - with sqlite3.connect(self.db_path) as db: - db.execute( - 'INSERT INTO repos (repo, ref, path) VALUES (?, ?, ?)', - [repo, ref, directory], - ) + def _get_result(): + # Check if we already exist + with sqlite3.connect(self.db_path) as db: + result = db.execute( + 'SELECT path FROM repos WHERE repo = ? AND ref = ?', + [repo, ref], + ).fetchone() + if result: + return result[0] + + result = _get_result() + if result: + return result + with self.exclusive_lock(): + # Another process may have already completed this work + result = _get_result() + if result: # pragma: no cover (race) + return result + + logger.info('Initializing environment for {}.'.format(repo)) + + directory = tempfile.mkdtemp(prefix='repo', dir=self.directory) + with clean_path_on_failure(directory): + make_strategy(directory) + + # Update our db with the created repo + with sqlite3.connect(self.db_path) as db: + db.execute( + 'INSERT INTO repos (repo, ref, path) VALUES (?, ?, ?)', + [repo, ref, directory], + ) return directory def clone(self, repo, ref): diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 94d396a92..bcf03076b 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -142,7 +142,8 @@ def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): NORMAL_PRE_COMMIT_RUN = re.compile( - r'^\[INFO\] Initializing environment for .+\.\r?\n' + r'^\[INFO\] Locking pre-commit directory\r?\n' + r'\[INFO\] Initializing environment for .+\.\r?\n' r'Bash hook\.+Passed\r?\n' r'\[master [a-f0-9]{7}\] Commit!\r?\n' + FILES_CHANGED + @@ -254,7 +255,8 @@ def test_environment_not_sourced(tempdir_factory): FAILING_PRE_COMMIT_RUN = re.compile( - r'^\[INFO\] Initializing environment for .+\.\r?\n' + r'^\[INFO\] Locking pre-commit directory\r?\n' + r'\[INFO\] Initializing environment for .+\.\r?\n' r'Failing hook\.+Failed\r?\n' r'hookid: failing_hook\r?\n' r'\r?\n' @@ -332,6 +334,7 @@ def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory): FAIL_OLD_HOOK = re.compile( r'fail!\r?\n' + r'\[INFO\] Locking pre-commit directory\r?\n' r'\[INFO\] Initializing environment for .+\.\r?\n' r'Bash hook\.+Passed\r?\n', ) diff --git a/tests/repository_test.py b/tests/repository_test.py index c35f66a37..9ad288624 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -547,8 +547,8 @@ def test_reinstall(tempdir_factory, store, log_info_mock): config = make_config_from_repo(path) repo = Repository.create(config, store) repo.require_installed() - # We print some logging during clone (1) + install (3) - assert log_info_mock.call_count == 4 + # We print some logging during clone (2) + install (4) + assert log_info_mock.call_count == 6 log_info_mock.reset_mock() # Reinstall with same repo should not trigger another install repo.require_installed() diff --git a/tests/store_test.py b/tests/store_test.py index eab4b009a..9a76a3397 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -88,7 +88,7 @@ def test_clone(store, tempdir_factory, log_info_mock): ret = store.clone(path, sha) # Should have printed some stuff - assert log_info_mock.call_args_list[0][0][0].startswith( + assert log_info_mock.call_args_list[1][0][0].startswith( 'Initializing environment for ', ) From 491b90548ff75eab9cab4717c6361570872d84cb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 24 Aug 2017 20:09:17 -0700 Subject: [PATCH 046/544] v0.17.0 --- CHANGELOG.md | 10 ++++++++++ setup.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86e5049ea..7deedb3f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +0.17.0 +====== +- Fix typos in help +- Allow `commit-msg` hook to be uninstalled +- Upgrade the `sample-config` +- Remove undocumented `--no-stash` and `--allow-unstaged-config` +- Remove `validate_config` hook pre-commit hook. +- Fix installation race condition when multiple `pre-commit` processes would + attempt to install the same repository. + 0.16.3 ====== - autoupdate attempts to maintain config formatting. diff --git a/setup.py b/setup.py index 1bb2651fd..ca842d7e2 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.16.3', + version='0.17.0', author='Anthony Sottile', author_email='asottile@umich.edu', From 9c3bbecab8768f82a963f3b4bec9c98ab20abec9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 2 Sep 2017 19:49:23 -0700 Subject: [PATCH 047/544] Add new docker_image language type. `docker_image` is intended to be a lightweight hook type similar to system / script which allows one to use an existing docker image which provides a hook. --- pre_commit/languages/all.py | 2 ++ pre_commit/languages/docker.py | 24 ++++++++++--------- pre_commit/languages/docker_image.py | 19 +++++++++++++++ pre_commit/languages/helpers.py | 4 ++++ pre_commit/languages/pcre.py | 6 +---- pre_commit/languages/script.py | 6 +---- pre_commit/languages/system.py | 6 +---- pre_commit/repository.py | 4 ++-- .../.pre-commit-hooks.yaml | 8 +++++++ tests/repository_test.py | 12 ++++++++++ 10 files changed, 63 insertions(+), 28 deletions(-) create mode 100644 pre_commit/languages/docker_image.py create mode 100644 testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 5de57fb8e..67b7ddea9 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from pre_commit.languages import docker +from pre_commit.languages import docker_image from pre_commit.languages import golang from pre_commit.languages import node from pre_commit.languages import pcre @@ -49,6 +50,7 @@ languages = { 'docker': docker, + 'docker_image': docker_image, 'golang': golang, 'node': node, 'pcre': pcre, diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index a9a0d342f..3dddf6189 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -77,6 +77,16 @@ def install_environment( os.mkdir(directory) +def docker_cmd(): + return ( + 'docker', 'run', + '--rm', + '-u', '{}:{}'.format(os.getuid(), os.getgid()), + '-v', '{}:/src:rw'.format(os.getcwd()), + '--workdir', '/src', + ) + + def run_hook(repo_cmd_runner, hook, file_args): # pragma: windows no cover assert_docker_available() # Rebuild the docker image in case it has gone missing, as many people do @@ -84,16 +94,8 @@ def run_hook(repo_cmd_runner, hook, file_args): # pragma: windows no cover build_docker_image(repo_cmd_runner, pull=False) hook_cmd = helpers.to_cmd(hook) - entry_executable, cmd_rest = hook_cmd[0], hook_cmd[1:] - - cmd = ( - 'docker', 'run', - '--rm', - '-u', '{}:{}'.format(os.getuid(), os.getgid()), - '-v', '{}:/src:rw'.format(os.getcwd()), - '--workdir', '/src', - '--entrypoint', entry_executable, - docker_tag(repo_cmd_runner), - ) + cmd_rest + entry_exe, cmd_rest = hook_cmd[0], hook_cmd[1:] + entry_tag = ('--entrypoint', entry_exe, docker_tag(repo_cmd_runner)) + cmd = docker_cmd() + entry_tag + cmd_rest return xargs(cmd, file_args) diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py new file mode 100644 index 000000000..a6f89e3f4 --- /dev/null +++ b/pre_commit/languages/docker_image.py @@ -0,0 +1,19 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +from pre_commit.languages import helpers +from pre_commit.languages.docker import assert_docker_available +from pre_commit.languages.docker import docker_cmd +from pre_commit.xargs import xargs + + +ENVIRONMENT_DIR = None +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy +install_environment = helpers.no_install + + +def run_hook(repo_cmd_runner, hook, file_args): # pragma: windows no cover + assert_docker_available() + cmd = docker_cmd() + helpers.to_cmd(hook) + return xargs(cmd, file_args) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 930a0755b..30082d6b4 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -41,3 +41,7 @@ def basic_get_default_version(): def basic_healthy(repo_cmd_runner, language_version): return True + + +def no_install(repo_cmd_runner, version, additional_dependencies): + raise AssertionError('This type is not installable') diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py index 6ef373f07..eaacc1101 100644 --- a/pre_commit/languages/pcre.py +++ b/pre_commit/languages/pcre.py @@ -10,11 +10,7 @@ GREP = 'ggrep' if sys.platform == 'darwin' else 'grep' get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy - - -def install_environment(repo_cmd_runner, version, additional_dependencies): - """Installation for pcre type is a noop.""" - raise AssertionError('Cannot install pcre repo.') +install_environment = helpers.no_install def run_hook(repo_cmd_runner, hook, file_args): diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 0bbb3091b..8c3b0c56b 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -7,11 +7,7 @@ ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy - - -def install_environment(repo_cmd_runner, version, additional_dependencies): - """Installation for script type is a noop.""" - raise AssertionError('Cannot install script repo.') +install_environment = helpers.no_install def run_hook(repo_cmd_runner, hook, file_args): diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 1f1688d8d..693a1601e 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -7,11 +7,7 @@ ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy - - -def install_environment(repo_cmd_runner, version, additional_dependencies): - """Installation for system type is a noop.""" - raise AssertionError('Cannot install system repo.') +install_environment = helpers.no_install def run_hook(repo_cmd_runner, hook, file_args): diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 18f902cb1..675c47161 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -202,8 +202,8 @@ class LocalRepository(Repository): def _cmd_runner_from_deps(self, language_name, deps): """local repositories have a cmd runner per hook""" language = languages[language_name] - # pcre / script / system do not have environments so they work out - # of the current directory + # pcre / script / system / docker_image do not have environments so + # they work out of the current directory if language.ENVIRONMENT_DIR is None: return PrefixedCommandRunner(git.get_root()) else: diff --git a/testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml new file mode 100644 index 000000000..1b385aa12 --- /dev/null +++ b/testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml @@ -0,0 +1,8 @@ +- id: echo-entrypoint + name: echo (via --entrypoint) + language: docker_image + entry: --entrypoint echo cogniteev/echo +- id: echo-cmd + name: echo (via cmd) + language: docker_image + entry: cogniteev/echo echo diff --git a/tests/repository_test.py b/tests/repository_test.py index 9ad288624..ae924a763 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -164,6 +164,18 @@ def test_run_a_failing_docker_hook(tempdir_factory, store): ) +@skipif_slowtests_false +@skipif_cant_run_docker +@pytest.mark.integration +@pytest.mark.parametrize('hook_id', ('echo-entrypoint', 'echo-cmd')) +def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): + _test_hook_repo( + tempdir_factory, store, 'docker_image_hooks_repo', + hook_id, + ['Hello World from docker'], b'Hello World from docker\n', + ) + + @skipif_slowtests_false @xfailif_windows_no_node @pytest.mark.integration From 4aa787db19980593c0f73711f7133b495c346da6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 2 Sep 2017 20:46:19 -0700 Subject: [PATCH 048/544] v0.18.0 --- CHANGELOG.md | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7deedb3f7..641340069 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +0.18.0 +====== +- Add a new `docker_image` language type. `docker_image` is intended to be a + lightweight hook type similar to `system` / `script` which allows one to use + an existing docker image that provides a hook. `docker_image` hooks can + also be used as repository `local` hooks. + 0.17.0 ====== - Fix typos in help diff --git a/setup.py b/setup.py index ca842d7e2..1ba39286c 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.17.0', + version='0.18.0', author='Anthony Sottile', author_email='asottile@umich.edu', From 7cb3e00731d583df16727fa046eea47c910f6d6a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 4 Sep 2017 11:27:47 -0700 Subject: [PATCH 049/544] Only print that the lock is being acquired when waiting --- pre_commit/file_lock.py | 40 ++++++++++++++---------- pre_commit/store.py | 9 +++--- tests/commands/install_uninstall_test.py | 7 ++--- tests/repository_test.py | 4 +-- tests/store_test.py | 2 +- 5 files changed, 34 insertions(+), 28 deletions(-) diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index 054ac529b..f33584c3d 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -12,18 +12,22 @@ _region = 0xffff @contextlib.contextmanager - def _locked(fileno): - while True: - try: - msvcrt.locking(fileno, msvcrt.LK_LOCK, _region) - except OSError as e: - # Locking violation. Returned when the _LK_LOCK or _LK_RLCK - # flag is specified and the file cannot be locked after 10 - # attempts. - if e.errno != errno.EDEADLOCK: - raise - else: - break + def _locked(fileno, blocked_cb): + try: + msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region) + except IOError: + blocked_cb() + while True: + try: + msvcrt.locking(fileno, msvcrt.LK_LOCK, _region) + except IOError as e: + # Locking violation. Returned when the _LK_LOCK or _LK_RLCK + # flag is specified and the file cannot be locked after 10 + # attempts. + if e.errno != errno.EDEADLOCK: + raise + else: + break try: yield @@ -38,8 +42,12 @@ def _locked(fileno): import fcntl @contextlib.contextmanager - def _locked(fileno): - fcntl.flock(fileno, fcntl.LOCK_EX) + def _locked(fileno, blocked_cb): + try: + fcntl.flock(fileno, fcntl.LOCK_EX | fcntl.LOCK_NB) + except IOError: + blocked_cb() + fcntl.flock(fileno, fcntl.LOCK_EX) try: yield finally: @@ -47,7 +55,7 @@ def _locked(fileno): @contextlib.contextmanager -def lock(path): +def lock(path, blocked_cb): with open(path, 'a+') as f: - with _locked(f.fileno()): + with _locked(f.fileno(), blocked_cb): yield diff --git a/pre_commit/store.py b/pre_commit/store.py index 29237870c..365ed9a1a 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -47,10 +47,11 @@ def __init__(self, directory=None): self.directory = directory @contextlib.contextmanager - def exclusive_lock(self, quiet=False): - if not quiet: + def exclusive_lock(self): + def blocked_cb(): # pragma: no cover (tests are single-process) logger.info('Locking pre-commit directory') - with file_lock.lock(os.path.join(self.directory, '.lock')): + + with file_lock.lock(os.path.join(self.directory, '.lock'), blocked_cb): yield def _write_readme(self): @@ -89,7 +90,7 @@ def _create(self): if os.path.exists(self.db_path): return - with self.exclusive_lock(quiet=True): + with self.exclusive_lock(): # Another process may have already completed this work if os.path.exists(self.db_path): # pragma: no cover (race) return diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index bcf03076b..94d396a92 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -142,8 +142,7 @@ def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): NORMAL_PRE_COMMIT_RUN = re.compile( - r'^\[INFO\] Locking pre-commit directory\r?\n' - r'\[INFO\] Initializing environment for .+\.\r?\n' + r'^\[INFO\] Initializing environment for .+\.\r?\n' r'Bash hook\.+Passed\r?\n' r'\[master [a-f0-9]{7}\] Commit!\r?\n' + FILES_CHANGED + @@ -255,8 +254,7 @@ def test_environment_not_sourced(tempdir_factory): FAILING_PRE_COMMIT_RUN = re.compile( - r'^\[INFO\] Locking pre-commit directory\r?\n' - r'\[INFO\] Initializing environment for .+\.\r?\n' + r'^\[INFO\] Initializing environment for .+\.\r?\n' r'Failing hook\.+Failed\r?\n' r'hookid: failing_hook\r?\n' r'\r?\n' @@ -334,7 +332,6 @@ def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory): FAIL_OLD_HOOK = re.compile( r'fail!\r?\n' - r'\[INFO\] Locking pre-commit directory\r?\n' r'\[INFO\] Initializing environment for .+\.\r?\n' r'Bash hook\.+Passed\r?\n', ) diff --git a/tests/repository_test.py b/tests/repository_test.py index ae924a763..6842800ea 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -559,8 +559,8 @@ def test_reinstall(tempdir_factory, store, log_info_mock): config = make_config_from_repo(path) repo = Repository.create(config, store) repo.require_installed() - # We print some logging during clone (2) + install (4) - assert log_info_mock.call_count == 6 + # We print some logging during clone (1) + install (3) + assert log_info_mock.call_count == 4 log_info_mock.reset_mock() # Reinstall with same repo should not trigger another install repo.require_installed() diff --git a/tests/store_test.py b/tests/store_test.py index 9a76a3397..eab4b009a 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -88,7 +88,7 @@ def test_clone(store, tempdir_factory, log_info_mock): ret = store.clone(path, sha) # Should have printed some stuff - assert log_info_mock.call_args_list[1][0][0].startswith( + assert log_info_mock.call_args_list[0][0][0].startswith( 'Initializing environment for ', ) From 3f7e715c20b50ae34c8cd332e42ef06080f4bf15 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 4 Sep 2017 12:44:38 -0700 Subject: [PATCH 050/544] v0.18.1 --- CHANGELOG.md | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 641340069..23071a0f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +0.18.1 +====== +- Only mention locking when waiting for a lock. +- Fix `IOError` during locking in timeout situtation on windows under python 2. + 0.18.0 ====== - Add a new `docker_image` language type. `docker_image` is intended to be a diff --git a/setup.py b/setup.py index 1ba39286c..183ec175c 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.18.0', + version='0.18.1', author='Anthony Sottile', author_email='asottile@umich.edu', From a9e1940f7e3376dad284416bbff95fdb064f8141 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 4 Sep 2017 13:42:19 -0700 Subject: [PATCH 051/544] Use SystemExit instead of PreCommitSystemExit --- pre_commit/error_handler.py | 7 +------ tests/error_handler_test.py | 4 ++-- tests/main_test.py | 3 +-- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index a661cc4fc..b248f9344 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -14,11 +14,6 @@ from pre_commit.store import Store -# For testing purposes -class PreCommitSystemExit(SystemExit): - pass - - def _to_bytes(exc): try: return bytes(exc) @@ -39,7 +34,7 @@ def _log_and_exit(msg, exc, formatted): with open(os.path.join(store.directory, 'pre-commit.log'), 'wb') as log: output.write(error_msg, stream=log) output.write_line(formatted, stream=log) - raise PreCommitSystemExit(1) + raise SystemExit(1) @contextlib.contextmanager diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 1d53c4b7d..bdc54b6a0 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -75,7 +75,7 @@ def test_error_handler_uncaught_error(mocked_log_and_exit): def test_log_and_exit(cap_out, mock_out_store_directory): - with pytest.raises(error_handler.PreCommitSystemExit): + with pytest.raises(SystemExit): error_handler._log_and_exit( 'msg', FatalError('hai'), "I'm a stacktrace", ) @@ -96,7 +96,7 @@ def test_log_and_exit(cap_out, mock_out_store_directory): def test_error_handler_non_ascii_exception(mock_out_store_directory): - with pytest.raises(error_handler.PreCommitSystemExit): + with pytest.raises(SystemExit): with error_handler.error_handler(): raise ValueError('☃') diff --git a/tests/main_test.py b/tests/main_test.py index 0425b8d27..4348b8ceb 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -7,7 +7,6 @@ import pytest from pre_commit import main -from pre_commit.error_handler import PreCommitSystemExit from pre_commit.util import cwd from testing.auto_namedtuple import auto_namedtuple @@ -120,7 +119,7 @@ def test_expected_fatal_error_no_git_repo( tempdir_factory, cap_out, mock_out_store_directory, ): with cwd(tempdir_factory.get()): - with pytest.raises(PreCommitSystemExit): + with pytest.raises(SystemExit): main.main([]) assert cap_out.get() == ( 'An error has occurred: FatalError: git failed. ' From f9a849abcc74d0edeeb857735ad1bfc18b6b31ab Mon Sep 17 00:00:00 2001 From: wanghui Date: Tue, 5 Sep 2017 16:22:33 +0800 Subject: [PATCH 052/544] Fix specify config file not work while installing Via `pre-commit install -c .other-config.yaml` --- pre_commit/commands/install_uninstall.py | 1 + pre_commit/resources/commit-msg-tmpl | 2 +- pre_commit/resources/hook-tmpl | 8 ++++---- pre_commit/resources/pre-push-tmpl | 6 +++--- tests/commands/install_uninstall_test.py | 2 ++ 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 6e09dabde..6d9d14d82 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -71,6 +71,7 @@ def install( sys_executable=sys.executable, hook_type=hook_type, hook_specific=hook_specific_contents, + config_file=runner.config_file, skip_on_missing_conf=skip_on_missing_conf, ) pre_commit_file_obj.write(contents) diff --git a/pre_commit/resources/commit-msg-tmpl b/pre_commit/resources/commit-msg-tmpl index b11521b01..182f214a5 100644 --- a/pre_commit/resources/commit-msg-tmpl +++ b/pre_commit/resources/commit-msg-tmpl @@ -1 +1 @@ -args="run --hook-stage=commit-msg --commit-msg-filename=$1" +args="--hook-stage=commit-msg --commit-msg-filename=$1" diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 3bfce5c79..78aa2a838 100644 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -38,13 +38,13 @@ if [ -x "$HERE"/{hook_type}.legacy ]; then fi fi -CONF_FILE=$(git rev-parse --show-toplevel)"/.pre-commit-config.yaml" +CONF_FILE=$(git rev-parse --show-toplevel)"/{config_file}" if [ ! -f $CONF_FILE ]; then if [ $SKIP_ON_MISSING_CONF = true ] || [ ! -z $PRE_COMMIT_ALLOW_NO_CONFIG ]; then - echo '`.pre-commit-config.yaml` config file not found. Skipping `pre-commit`.' + echo '`{config_file}` config file not found. Skipping `pre-commit`.' exit $retv else - echo 'No .pre-commit-config.yaml file was found' + echo 'No {config_file} file was found' echo '- To temporarily silence this, run `PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`' echo '- To permanently silence this, install pre-commit with the `--allow-missing-config` option' echo '- To uninstall pre-commit run `pre-commit uninstall`' @@ -56,7 +56,7 @@ fi # Run pre-commit if ((WHICH_RETV == 0)); then - pre-commit $args + pre-commit run $args -c {config_file} PRE_COMMIT_RETV=$? elif ((ENV_PYTHON_RETV == 0)); then "$ENV_PYTHON" -m pre_commit.main $args diff --git a/pre_commit/resources/pre-push-tmpl b/pre_commit/resources/pre-push-tmpl index 81d0dcbed..f866eeff2 100644 --- a/pre_commit/resources/pre-push-tmpl +++ b/pre_commit/resources/pre-push-tmpl @@ -9,14 +9,14 @@ do # Check that the ancestor has at least one parent git rev-list --max-parents=0 "$local_sha" | grep "$first_ancestor" > /dev/null if [ $? -ne 0 ]; then - args="run --all-files" + args="--all-files" else source=$(git rev-parse "$first_ancestor"^) - args="run --origin $local_sha --source $source" + args="--origin $local_sha --source $source" fi fi else - args="run --origin $local_sha --source $remote_sha" + args="--origin $local_sha --source $remote_sha" fi fi done diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 94d396a92..357131c53 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -57,6 +57,7 @@ def test_install_pre_commit(tempdir_factory): sys_executable=sys.executable, hook_type='pre-commit', hook_specific='', + config_file=runner.config_file, skip_on_missing_conf='false', ) assert pre_commit_contents == expected_contents @@ -72,6 +73,7 @@ def test_install_pre_commit(tempdir_factory): sys_executable=sys.executable, hook_type='pre-push', hook_specific=pre_push_template_contents, + config_file=runner.config_file, skip_on_missing_conf='false', ) assert pre_push_contents == expected_contents From 0815108242217c31cc035c4ac1f1ea6aee4bf013 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 5 Sep 2017 08:04:39 -0700 Subject: [PATCH 053/544] Handle non-ascii filenames from git --- pre_commit/commands/run.py | 9 +----- pre_commit/git.py | 31 ++++++++++++++----- tests/commands/run_test.py | 13 -------- tests/git_test.py | 61 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 29 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index ac418a787..8b80bef05 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -36,13 +36,6 @@ def _hook_msg_start(hook, verbose): ) -def get_changed_files(new, old): - return cmd_output( - 'git', 'diff', '--no-ext-diff', '--name-only', - '{}...{}'.format(old, new), - )[1].splitlines() - - def filter_filenames_by_types(filenames, types, exclude_types): types, exclude_types = frozenset(types), frozenset(exclude_types) ret = [] @@ -56,7 +49,7 @@ def filter_filenames_by_types(filenames, types, exclude_types): def get_filenames(args, include_expr, exclude_expr): if args.origin and args.source: getter = git.get_files_matching( - lambda: get_changed_files(args.origin, args.source), + lambda: git.get_changed_files(args.origin, args.source), ) elif args.hook_stage == 'commit-msg': def getter(*_): diff --git a/pre_commit/git.py b/pre_commit/git.py index 4b519c86a..cdf807b58 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -15,6 +15,14 @@ logger = logging.getLogger('pre_commit') +def zsplit(s): + s = s.strip('\0') + if s: + return s.split('\0') + else: + return [] + + def get_root(): try: return cmd_output('git', 'rev-parse', '--show-toplevel')[1].strip() @@ -67,25 +75,32 @@ def get_conflicted_files(): # If they resolved the merge conflict by choosing a mesh of both sides # this will also include the conflicted files tree_hash = cmd_output('git', 'write-tree')[1].strip() - merge_diff_filenames = cmd_output( - 'git', 'diff', '--no-ext-diff', - '-m', tree_hash, 'HEAD', 'MERGE_HEAD', '--name-only', - )[1].splitlines() + merge_diff_filenames = zsplit(cmd_output( + 'git', 'diff', '--name-only', '--no-ext-diff', '-z', + '-m', tree_hash, 'HEAD', 'MERGE_HEAD', + )[1]) return set(merge_conflict_filenames) | set(merge_diff_filenames) @memoize_by_cwd def get_staged_files(): - return cmd_output( - 'git', 'diff', '--staged', '--name-only', '--no-ext-diff', + return zsplit(cmd_output( + 'git', 'diff', '--staged', '--name-only', '--no-ext-diff', '-z', # Everything except for D '--diff-filter=ACMRTUXB', - )[1].splitlines() + )[1]) @memoize_by_cwd def get_all_files(): - return cmd_output('git', 'ls-files')[1].splitlines() + return zsplit(cmd_output('git', 'ls-files', '-z')[1]) + + +def get_changed_files(new, old): + return zsplit(cmd_output( + 'git', 'diff', '--name-only', '--no-ext-diff', '-z', + '{}...{}'.format(old, new), + )[1]) def get_files_matching(all_file_list_strategy): diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 924d097fc..5ed0ad8a4 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -15,7 +15,6 @@ from pre_commit.commands.run import _compute_cols from pre_commit.commands.run import _get_skips from pre_commit.commands.run import _has_unmerged_paths -from pre_commit.commands.run import get_changed_files from pre_commit.commands.run import run from pre_commit.runner import Runner from pre_commit.util import cmd_output @@ -501,18 +500,6 @@ def test_hook_install_failure(mock_out_store_directory, tempdir_factory): assert '☃'.encode('UTF-8') + '²'.encode('latin1') in stdout -def test_get_changed_files(): - files = get_changed_files( - '78c682a1d13ba20e7cb735313b9314a74365cd3a', - '3387edbb1288a580b37fe25225aa0b856b18ad1a', - ) - assert files == ['CHANGELOG.md', 'setup.py'] - - # files changed in source but not in origin should not be returned - files = get_changed_files('HEAD~10', 'HEAD') - assert files == [] - - def test_lots_of_files(mock_out_store_directory, tempdir_factory): # windows xargs seems to have a bug, here's a regression test for # our workaround diff --git a/tests/git_test.py b/tests/git_test.py index 0500a42df..4f6791192 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import unicode_literals @@ -162,3 +163,63 @@ def test_get_conflicted_files_unstaged_files(in_merge_conflict): def test_parse_merge_msg_for_conflicts(input, expected_output): ret = git.parse_merge_msg_for_conflicts(input) assert ret == expected_output + + +def test_get_changed_files(): + files = git.get_changed_files( + '78c682a1d13ba20e7cb735313b9314a74365cd3a', + '3387edbb1288a580b37fe25225aa0b856b18ad1a', + ) + assert files == ['CHANGELOG.md', 'setup.py'] + + # files changed in source but not in origin should not be returned + files = git.get_changed_files('HEAD~10', 'HEAD') + assert files == [] + + +@pytest.mark.parametrize( + ('s', 'expected'), + ( + ('foo\0bar\0', ['foo', 'bar']), + ('foo\0', ['foo']), + ('', []), + ('foo', ['foo']), + ), +) +def test_zsplit(s, expected): + assert git.zsplit(s) == expected + + +@pytest.fixture +def non_ascii_repo(tmpdir): + repo = tmpdir.join('repo').ensure_dir() + with repo.as_cwd(): + cmd_output('git', 'init', '.') + cmd_output('git', 'commit', '--allow-empty', '-m', 'initial commit') + repo.join('интервью').ensure() + cmd_output('git', 'add', '.') + cmd_output('git', 'commit', '--allow-empty', '-m', 'initial commit') + yield repo + + +def test_all_files_non_ascii(non_ascii_repo): + ret = git.get_all_files() + assert ret == ['интервью'] + + +def test_staged_files_non_ascii(non_ascii_repo): + non_ascii_repo.join('интервью').write('hi') + cmd_output('git', 'add', '.') + assert git.get_staged_files() == ['интервью'] + + +def test_changed_files_non_ascii(non_ascii_repo): + ret = git.get_changed_files('HEAD', 'HEAD^') + assert ret == ['интервью'] + + +def test_get_conflicted_files_non_ascii(in_merge_conflict): + open('интервью', 'a').close() + cmd_output('git', 'add', '.') + ret = git.get_conflicted_files() + assert ret == {'conflict_file', 'интервью'} From 50564480fb98fc555f1e6abc97b75a79b7b2846d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 5 Sep 2017 08:40:13 -0700 Subject: [PATCH 054/544] v0.18.2 --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23071a0f5..c12396a6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +0.18.2 +====== +- Fix `--all-files`, detection of staged files, detection of manually edited + files during merge conflict, and detection of files to push for non-ascii + filenames. + 0.18.1 ====== - Only mention locking when waiting for a lock. diff --git a/setup.py b/setup.py index 183ec175c..e6ea9b9dc 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.18.1', + version='0.18.2', author='Anthony Sottile', author_email='asottile@umich.edu', From eb7c9f44b47897216356f9bf488bc04fcbf505e1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 5 Sep 2017 09:27:00 -0700 Subject: [PATCH 055/544] Add test for custom config installation --- pre_commit/resources/hook-tmpl | 4 ++-- tests/commands/install_uninstall_test.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 78aa2a838..e18812ff9 100644 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -38,7 +38,7 @@ if [ -x "$HERE"/{hook_type}.legacy ]; then fi fi -CONF_FILE=$(git rev-parse --show-toplevel)"/{config_file}" +CONF_FILE="$(git rev-parse --show-toplevel)/{config_file}" if [ ! -f $CONF_FILE ]; then if [ $SKIP_ON_MISSING_CONF = true ] || [ ! -z $PRE_COMMIT_ALLOW_NO_CONFIG ]; then echo '`{config_file}` config file not found. Skipping `pre-commit`.' @@ -56,7 +56,7 @@ fi # Run pre-commit if ((WHICH_RETV == 0)); then - pre-commit run $args -c {config_file} + pre-commit run $args --config {config_file} PRE_COMMIT_RETV=$? elif ((ENV_PYTHON_RETV == 0)); then "$ENV_PYTHON" -m pre_commit.main $args diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 357131c53..80e249bea 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -162,6 +162,18 @@ def test_install_pre_commit_and_run(tempdir_factory): assert NORMAL_PRE_COMMIT_RUN.match(output) +def test_install_pre_commit_and_run_custom_path(tempdir_factory): + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + with cwd(path): + cmd_output('git', 'mv', C.CONFIG_FILE, 'custom-config.yaml') + cmd_output('git', 'commit', '-m', 'move pre-commit config') + assert install(Runner(path, 'custom-config.yaml')) == 0 + + ret, output = _get_commit_output(tempdir_factory) + assert ret == 0 + assert NORMAL_PRE_COMMIT_RUN.match(output) + + def test_install_in_submodule_and_run(tempdir_factory): src_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') parent_path = git_dir(tempdir_factory) From 95f356d64eda7b6db52c8ece7b2eb36b4f84719b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 5 Sep 2017 09:29:18 -0700 Subject: [PATCH 056/544] Also add run to the other invocations --- pre_commit/resources/hook-tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index e18812ff9..7e48d1eaa 100644 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -59,10 +59,10 @@ if ((WHICH_RETV == 0)); then pre-commit run $args --config {config_file} PRE_COMMIT_RETV=$? elif ((ENV_PYTHON_RETV == 0)); then - "$ENV_PYTHON" -m pre_commit.main $args + "$ENV_PYTHON" -m pre_commit.main run $args PRE_COMMIT_RETV=$? else - python -m pre_commit.main $args + python -m pre_commit.main run $args PRE_COMMIT_RETV=$? fi From 7c59607d35388b167ddbd42ab86aae2ac0a3d876 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 5 Sep 2017 09:33:51 -0700 Subject: [PATCH 057/544] Fix error message during pre-push / commit-msg --- pre_commit/resources/hook-tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 3bfce5c79..5d95e0fc4 100644 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -25,7 +25,7 @@ if (( (ENV_PYTHON_RETV != 0) && (PYTHON_RETV != 0) )); then - echo '`{hook_type}` not found. Did you forget to activate your virtualenv?' + echo '`pre-commit` not found. Did you forget to activate your virtualenv?' exit 1 fi From 95c3afacdae3e4439b603d54167267e7f237fd90 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 5 Sep 2017 14:07:27 -0700 Subject: [PATCH 058/544] Config loading uses ordered_load by default --- pre_commit/commands/autoupdate.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 36df87f8c..1c79500d3 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -107,10 +107,7 @@ def autoupdate(runner, tags_only): output_configs = [] changed = False - input_configs = load_config( - runner.config_file_path, - load_strategy=ordered_load, - ) + input_configs = load_config(runner.config_file_path) for repo_config in input_configs: if is_local_repo(repo_config): From 6141c419ee9a2d5d798dcf69e9be3e4582c8130c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 5 Sep 2017 14:49:31 -0700 Subject: [PATCH 059/544] Remove Runner.cmd_runner and Store.cmd_runner --- pre_commit/commands/run.py | 15 ++--- pre_commit/runner.py | 5 -- pre_commit/staged_files_only.py | 35 +++++------ pre_commit/store.py | 5 -- tests/commands/run_test.py | 10 ++- tests/runner_test.py | 6 -- tests/staged_files_only_test.py | 104 ++++++++++++-------------------- tests/store_test.py | 5 -- 8 files changed, 65 insertions(+), 120 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 8b80bef05..99232585e 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -192,17 +192,14 @@ def get_repo_hooks(runner): yield (repo, hook) -def _has_unmerged_paths(runner): - _, stdout, _ = runner.cmd_runner.run(['git', 'ls-files', '--unmerged']) +def _has_unmerged_paths(): + _, stdout, _ = cmd_output('git', 'ls-files', '--unmerged') return bool(stdout.strip()) def _has_unstaged_config(runner): - retcode, _, _ = runner.cmd_runner.run( - ( - 'git', 'diff', '--no-ext-diff', '--exit-code', - runner.config_file_path, - ), + retcode, _, _ = cmd_output( + 'git', 'diff', '--no-ext-diff', '--exit-code', runner.config_file_path, retcode=None, ) # be explicit, other git errors don't mean it has an unstaged config. @@ -213,7 +210,7 @@ def run(runner, args, environ=os.environ): no_stash = args.all_files or bool(args.files) # Check if we have unresolved merge conflict files and fail fast. - if _has_unmerged_paths(runner): + if _has_unmerged_paths(): logger.error('Unmerged files. Resolve before committing.') return 1 if bool(args.source) != bool(args.origin): @@ -234,7 +231,7 @@ def run(runner, args, environ=os.environ): if no_stash: ctx = noop_context() else: - ctx = staged_files_only(runner.cmd_runner) + ctx = staged_files_only(runner.store.directory) with ctx: repo_hooks = list(get_repo_hooks(runner)) diff --git a/pre_commit/runner.py b/pre_commit/runner.py index c7455d718..21707cb4c 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -57,11 +57,6 @@ def pre_commit_path(self): def pre_push_path(self): return self.get_hook_path('pre-push') - @cached_property - def cmd_runner(self): - # TODO: remove this and inline runner.store.cmd_runner - return self.store.cmd_runner - @cached_property def store(self): return Store() diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 4d233924f..cfd638155 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -3,44 +3,41 @@ import contextlib import io import logging +import os.path import time from pre_commit.util import CalledProcessError +from pre_commit.util import cmd_output logger = logging.getLogger('pre_commit') -def _git_apply(cmd_runner, patch): +def _git_apply(patch): args = ('apply', '--whitespace=nowarn', patch) try: - cmd_runner.run(('git',) + args, encoding=None) + cmd_output('git', *args, encoding=None) except CalledProcessError: # Retry with autocrlf=false -- see #570 - cmd = ('git', '-c', 'core.autocrlf=false') + args - cmd_runner.run(cmd, encoding=None) + cmd_output('git', '-c', 'core.autocrlf=false', *args, encoding=None) @contextlib.contextmanager -def staged_files_only(cmd_runner): +def staged_files_only(patch_dir): """Clear any unstaged changes from the git working directory inside this context. - - Args: - cmd_runner - PrefixedCommandRunner """ # Determine if there are unstaged files - tree = cmd_runner.run(('git', 'write-tree'))[1].strip() - retcode, diff_stdout_binary, _ = cmd_runner.run( - ( - 'git', 'diff-index', '--ignore-submodules', '--binary', - '--exit-code', '--no-color', '--no-ext-diff', tree, '--', - ), + tree = cmd_output('git', 'write-tree')[1].strip() + retcode, diff_stdout_binary, _ = cmd_output( + 'git', 'diff-index', '--ignore-submodules', '--binary', + '--exit-code', '--no-color', '--no-ext-diff', tree, '--', retcode=None, encoding=None, ) if retcode and diff_stdout_binary.strip(): - patch_filename = cmd_runner.path('patch{}'.format(int(time.time()))) + patch_filename = 'patch{}'.format(int(time.time())) + patch_filename = os.path.join(patch_dir, patch_filename) logger.warning('Unstaged files detected.') logger.info( 'Stashing unstaged files to {}.'.format(patch_filename), @@ -50,13 +47,13 @@ def staged_files_only(cmd_runner): patch_file.write(diff_stdout_binary) # Clear the working directory of unstaged changes - cmd_runner.run(('git', 'checkout', '--', '.')) + cmd_output('git', 'checkout', '--', '.') try: yield finally: # Try to apply the patch we saved try: - _git_apply(cmd_runner, patch_filename) + _git_apply(patch_filename) except CalledProcessError: logger.warning( 'Stashed changes conflicted with hook auto-fixes... ' @@ -65,8 +62,8 @@ def staged_files_only(cmd_runner): # We failed to apply the patch, presumably due to fixes made # by hooks. # Roll back the changes made by hooks. - cmd_runner.run(('git', 'checkout', '--', '.')) - _git_apply(cmd_runner, patch_filename) + cmd_output('git', 'checkout', '--', '.') + _git_apply(patch_filename) logger.info('Restored changes from {}.'.format(patch_filename)) else: # There weren't any staged files so we don't need to do anything diff --git a/pre_commit/store.py b/pre_commit/store.py index 365ed9a1a..263b315bd 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -11,7 +11,6 @@ import pre_commit.constants as C from pre_commit import file_lock -from pre_commit.prefixed_command_runner import PrefixedCommandRunner from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import copy_tree_to_path @@ -162,10 +161,6 @@ def make_local_strategy(directory): make_local_strategy, ) - @cached_property - def cmd_runner(self): - return PrefixedCommandRunner(self.directory) - @cached_property def db_path(self): return os.path.join(self.directory, 'db.db') diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 5ed0ad8a4..b544eb75f 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -7,7 +7,6 @@ import sys from collections import OrderedDict -import mock import pytest import pre_commit.constants as C @@ -327,11 +326,10 @@ def test_origin_source_error_msg( assert warning_msg not in printed -@pytest.mark.parametrize(('output', 'expected'), (('some', True), ('', False))) -def test_has_unmerged_paths(output, expected): - mock_runner = mock.Mock() - mock_runner.cmd_runner.run.return_value = (1, output, '') - assert _has_unmerged_paths(mock_runner) is expected +def test_has_unmerged_paths(in_merge_conflict): + assert _has_unmerged_paths() is True + cmd_output('git', 'add', '.') + assert _has_unmerged_paths() is False def test_merge_conflict(cap_out, in_merge_conflict, mock_out_store_directory): diff --git a/tests/runner_test.py b/tests/runner_test.py index 0201156c5..cfca44f36 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -133,9 +133,3 @@ def test_pre_push_path(in_tmpdir): runner = Runner(path, C.CONFIG_FILE) expected_path = os.path.join(path, '.git', 'hooks', 'pre-push') assert runner.pre_push_path == expected_path - - -def test_cmd_runner(mock_out_store_directory): - runner = Runner(os.path.join('foo', 'bar'), C.CONFIG_FILE) - ret = runner.cmd_runner - assert ret.prefix_dir == os.path.join(mock_out_store_directory) + os.sep diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 78926d051..aec55f5de 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -4,11 +4,9 @@ import io import itertools -import logging import os.path import shutil -import mock import pytest from pre_commit.staged_files_only import staged_files_only @@ -22,6 +20,11 @@ FOO_CONTENTS = '\n'.join(('1', '2', '3', '4', '5', '6', '7', '8', '')) +@pytest.fixture +def patch_dir(tempdir_factory): + return tempdir_factory.get() + + def get_short_git_status(): git_status = cmd_output('git', 'status', '-s')[1] return dict(reversed(line.split()) for line in git_status.splitlines()) @@ -54,43 +57,43 @@ def test_foo_staged(foo_staged): _test_foo_state(foo_staged) -def test_foo_nothing_unstaged(foo_staged, cmd_runner): - with staged_files_only(cmd_runner): +def test_foo_nothing_unstaged(foo_staged, patch_dir): + with staged_files_only(patch_dir): _test_foo_state(foo_staged) _test_foo_state(foo_staged) -def test_foo_something_unstaged(foo_staged, cmd_runner): +def test_foo_something_unstaged(foo_staged, patch_dir): with io.open(foo_staged.foo_filename, 'w') as foo_file: foo_file.write('herp\nderp\n') _test_foo_state(foo_staged, 'herp\nderp\n', 'AM') - with staged_files_only(cmd_runner): + with staged_files_only(patch_dir): _test_foo_state(foo_staged) _test_foo_state(foo_staged, 'herp\nderp\n', 'AM') -def test_something_unstaged_ext_diff_tool(foo_staged, cmd_runner, tmpdir): +def test_something_unstaged_ext_diff_tool(foo_staged, patch_dir, tmpdir): diff_tool = tmpdir.join('diff-tool.sh') diff_tool.write('#!/usr/bin/env bash\necho "$@"\n') cmd_output('git', 'config', 'diff.external', diff_tool.strpath) - test_foo_something_unstaged(foo_staged, cmd_runner) + test_foo_something_unstaged(foo_staged, patch_dir) -def test_foo_something_unstaged_diff_color_always(foo_staged, cmd_runner): +def test_foo_something_unstaged_diff_color_always(foo_staged, patch_dir): cmd_output('git', 'config', '--local', 'color.diff', 'always') - test_foo_something_unstaged(foo_staged, cmd_runner) + test_foo_something_unstaged(foo_staged, patch_dir) -def test_foo_both_modify_non_conflicting(foo_staged, cmd_runner): +def test_foo_both_modify_non_conflicting(foo_staged, patch_dir): with io.open(foo_staged.foo_filename, 'w') as foo_file: foo_file.write(FOO_CONTENTS + '9\n') _test_foo_state(foo_staged, FOO_CONTENTS + '9\n', 'AM') - with staged_files_only(cmd_runner): + with staged_files_only(patch_dir): _test_foo_state(foo_staged) # Modify the file as part of the "pre-commit" @@ -102,13 +105,13 @@ def test_foo_both_modify_non_conflicting(foo_staged, cmd_runner): _test_foo_state(foo_staged, FOO_CONTENTS.replace('1', 'a') + '9\n', 'AM') -def test_foo_both_modify_conflicting(foo_staged, cmd_runner): +def test_foo_both_modify_conflicting(foo_staged, patch_dir): with io.open(foo_staged.foo_filename, 'w') as foo_file: foo_file.write(FOO_CONTENTS.replace('1', 'a')) _test_foo_state(foo_staged, FOO_CONTENTS.replace('1', 'a'), 'AM') - with staged_files_only(cmd_runner): + with staged_files_only(patch_dir): _test_foo_state(foo_staged) # Modify in the same place as the stashed diff @@ -144,30 +147,30 @@ def test_img_staged(img_staged): _test_img_state(img_staged) -def test_img_nothing_unstaged(img_staged, cmd_runner): - with staged_files_only(cmd_runner): +def test_img_nothing_unstaged(img_staged, patch_dir): + with staged_files_only(patch_dir): _test_img_state(img_staged) _test_img_state(img_staged) -def test_img_something_unstaged(img_staged, cmd_runner): +def test_img_something_unstaged(img_staged, patch_dir): shutil.copy(get_resource_path('img2.jpg'), img_staged.img_filename) _test_img_state(img_staged, 'img2.jpg', 'AM') - with staged_files_only(cmd_runner): + with staged_files_only(patch_dir): _test_img_state(img_staged) _test_img_state(img_staged, 'img2.jpg', 'AM') -def test_img_conflict(img_staged, cmd_runner): +def test_img_conflict(img_staged, patch_dir): """Admittedly, this shouldn't happen, but just in case.""" shutil.copy(get_resource_path('img2.jpg'), img_staged.img_filename) _test_img_state(img_staged, 'img2.jpg', 'AM') - with staged_files_only(cmd_runner): + with staged_files_only(patch_dir): _test_img_state(img_staged) shutil.copy(get_resource_path('img3.jpg'), img_staged.img_filename) _test_img_state(img_staged, 'img3.jpg', 'AM') @@ -220,77 +223,48 @@ def test_sub_staged(sub_staged): _test_sub_state(sub_staged) -def test_sub_nothing_unstaged(sub_staged, cmd_runner): - with staged_files_only(cmd_runner): +def test_sub_nothing_unstaged(sub_staged, patch_dir): + with staged_files_only(patch_dir): _test_sub_state(sub_staged) _test_sub_state(sub_staged) -def test_sub_something_unstaged(sub_staged, cmd_runner): +def test_sub_something_unstaged(sub_staged, patch_dir): checkout_submodule(sub_staged.submodule.sha2) _test_sub_state(sub_staged, 'sha2', 'AM') - with staged_files_only(cmd_runner): + with staged_files_only(patch_dir): # This is different from others, we don't want to touch subs _test_sub_state(sub_staged, 'sha2', 'AM') _test_sub_state(sub_staged, 'sha2', 'AM') -@pytest.yield_fixture -def fake_logging_handler(): - class FakeHandler(logging.Handler): - def __init__(self): - logging.Handler.__init__(self) - self.logs = [] - - def emit(self, record): - self.logs.append(record) # pragma: no cover (only hit in failure) - - pre_commit_logger = logging.getLogger('pre_commit') - original_level = pre_commit_logger.getEffectiveLevel() - handler = FakeHandler() - pre_commit_logger.addHandler(handler) - pre_commit_logger.setLevel(logging.WARNING) - yield handler - pre_commit_logger.setLevel(original_level) - pre_commit_logger.removeHandler(handler) - - -def test_diff_returns_1_no_diff_though(fake_logging_handler, foo_staged): - cmd_runner = mock.Mock() - cmd_runner.run.return_value = (1, '', '') - cmd_runner.path.return_value = '.pre-commit-files_patch' - with staged_files_only(cmd_runner): - pass - assert not fake_logging_handler.logs - - -def test_stage_utf8_changes(foo_staged, cmd_runner): +def test_stage_utf8_changes(foo_staged, patch_dir): contents = '\u2603' with io.open('foo', 'w', encoding='UTF-8') as foo_file: foo_file.write(contents) _test_foo_state(foo_staged, contents, 'AM') - with staged_files_only(cmd_runner): + with staged_files_only(patch_dir): _test_foo_state(foo_staged) _test_foo_state(foo_staged, contents, 'AM') -def test_stage_non_utf8_changes(foo_staged, cmd_runner): +def test_stage_non_utf8_changes(foo_staged, patch_dir): contents = 'ú' # Produce a latin-1 diff with io.open('foo', 'w', encoding='latin-1') as foo_file: foo_file.write(contents) _test_foo_state(foo_staged, contents, 'AM', encoding='latin-1') - with staged_files_only(cmd_runner): + with staged_files_only(patch_dir): _test_foo_state(foo_staged) _test_foo_state(foo_staged, contents, 'AM', encoding='latin-1') -def test_non_utf8_conflicting_diff(foo_staged, cmd_runner): +def test_non_utf8_conflicting_diff(foo_staged, patch_dir): """Regression test for #397""" # The trailing whitespace is important here, this triggers git to produce # an error message which looks like: @@ -307,7 +281,7 @@ def test_non_utf8_conflicting_diff(foo_staged, cmd_runner): foo_file.write(contents) _test_foo_state(foo_staged, contents, 'AM', encoding='latin-1') - with staged_files_only(cmd_runner): + with staged_files_only(patch_dir): _test_foo_state(foo_staged) # Create a conflicting diff that will need to be rolled back with io.open('foo', 'w') as foo_file: @@ -337,7 +311,7 @@ def assert_no_diff(): @pytest.mark.parametrize(('crlf_before', 'crlf_after'), bool_product) @pytest.mark.parametrize('autocrlf', ('true', 'false', 'input')) -def test_crlf(in_git_dir, cmd_runner, crlf_before, crlf_after, autocrlf): +def test_crlf(in_git_dir, patch_dir, crlf_before, crlf_after, autocrlf): cmd_output('git', 'config', '--local', 'core.autocrlf', autocrlf) before, after = b'1\n2\n', b'3\n4\n\n' @@ -347,16 +321,16 @@ def test_crlf(in_git_dir, cmd_runner, crlf_before, crlf_after, autocrlf): _write(before) cmd_output('git', 'add', 'foo') _write(after) - with staged_files_only(cmd_runner): + with staged_files_only(patch_dir): assert_no_diff() -def test_whitespace_errors(in_git_dir, cmd_runner): +def test_whitespace_errors(in_git_dir, patch_dir): cmd_output('git', 'config', '--local', 'apply.whitespace', 'error') - test_crlf(in_git_dir, cmd_runner, True, True, 'true') + test_crlf(in_git_dir, patch_dir, True, True, 'true') -def test_autocrlf_commited_crlf(in_git_dir, cmd_runner): +def test_autocrlf_commited_crlf(in_git_dir, patch_dir): """Regression test for #570""" cmd_output('git', 'config', '--local', 'core.autocrlf', 'false') _write(b'1\r\n2\r\n') @@ -366,5 +340,5 @@ def test_autocrlf_commited_crlf(in_git_dir, cmd_runner): cmd_output('git', 'config', '--local', 'core.autocrlf', 'true') _write(b'1\r\n2\r\n\r\n\r\n\r\n') - with staged_files_only(cmd_runner): + with staged_files_only(patch_dir): assert_no_diff() diff --git a/tests/store_test.py b/tests/store_test.py index eab4b009a..106a4645e 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -125,11 +125,6 @@ def test_clone_cleans_up_on_checkout_failure(store): assert things_starting_with_repo == [] -def test_has_cmd_runner_at_directory(store): - ret = store.cmd_runner - assert ret.prefix_dir == store.directory + os.sep - - def test_clone_when_repo_already_exists(store): # Create an entry in the sqlite db that makes it look like the repo has # been cloned. From 68ce070b65b75667d00ddc3a5acd4c4eabe0083b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 5 Sep 2017 16:40:08 -0700 Subject: [PATCH 060/544] Remove --unshallow fetch from travis-ci config --- .travis.yml | 2 -- tests/git_test.py | 17 ++++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 900446d29..e84d8cc1e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,8 +16,6 @@ matrix: install: pip install coveralls tox script: tox before_install: - # Our tests inspect some of *our* git history - - git fetch --unshallow - git --version - | if [ "$LATEST_GIT" = "1" ]; then diff --git a/tests/git_test.py b/tests/git_test.py index 4f6791192..4fce5ab08 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -165,15 +165,18 @@ def test_parse_merge_msg_for_conflicts(input, expected_output): assert ret == expected_output -def test_get_changed_files(): - files = git.get_changed_files( - '78c682a1d13ba20e7cb735313b9314a74365cd3a', - '3387edbb1288a580b37fe25225aa0b856b18ad1a', - ) - assert files == ['CHANGELOG.md', 'setup.py'] +def test_get_changed_files(in_tmpdir): + cmd_output('git', 'init', '.') + cmd_output('git', 'commit', '--allow-empty', '-m', 'initial commit') + open('a.txt', 'a').close() + open('b.txt', 'a').close() + cmd_output('git', 'add', '.') + cmd_output('git', 'commit', '-m', 'add some files') + files = git.get_changed_files('HEAD', 'HEAD^') + assert files == ['a.txt', 'b.txt'] # files changed in source but not in origin should not be returned - files = git.get_changed_files('HEAD~10', 'HEAD') + files = git.get_changed_files('HEAD^', 'HEAD') assert files == [] From e465129bd4e24df1b1a2ba9fc2e41a0287533bb3 Mon Sep 17 00:00:00 2001 From: Lucas Cimon Date: Wed, 6 Sep 2017 16:21:25 +0200 Subject: [PATCH 061/544] NodeJS hooks compatibilty fix for Cygwin - take 2 --- pre_commit/languages/node.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 58922672b..49822dde3 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -8,6 +8,7 @@ from pre_commit.envcontext import Var from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure +from pre_commit.util import cmd_output from pre_commit.xargs import xargs @@ -16,12 +17,16 @@ healthy = helpers.basic_healthy -def get_env_patch(venv): # pragma: windows no cover - config = os.path.join(venv, 'bin') if sys.platform == 'cygwin' else venv +def get_env_patch(venv): # pragma: windows no cover + if sys.platform == 'cygwin': # pragma: no cover + _, win_venv, _ = cmd_output('cygpath', '-w', venv) + install_prefix = r'{}\bin'.format(win_venv.strip()) + else: + install_prefix = venv return ( ('NODE_VIRTUAL_ENV', venv), - ('NPM_CONFIG_PREFIX', config), - ('npm_config_prefix', config), + ('NPM_CONFIG_PREFIX', install_prefix), + ('npm_config_prefix', install_prefix), ('NODE_PATH', os.path.join(venv, 'lib', 'node_modules')), ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), ) From 92f98088ebcc49ebe652df199432b0c89eba7a1e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 6 Sep 2017 08:28:50 -0700 Subject: [PATCH 062/544] Whitespace fixup --- pre_commit/languages/node.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 49822dde3..aca3c4100 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -17,8 +17,8 @@ healthy = helpers.basic_healthy -def get_env_patch(venv): # pragma: windows no cover - if sys.platform == 'cygwin': # pragma: no cover +def get_env_patch(venv): # pragma: windows no cover + if sys.platform == 'cygwin': # pragma: no cover _, win_venv, _ = cmd_output('cygpath', '-w', venv) install_prefix = r'{}\bin'.format(win_venv.strip()) else: From 0120af56a776402454579bf38ab99ec972d87686 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 4 Sep 2017 13:39:12 -0700 Subject: [PATCH 063/544] Adhere to XDG specification for cache dir. --- .travis.yml | 2 +- appveyor.yml | 2 +- pre_commit/commands/clean.py | 8 +++++--- pre_commit/error_handler.py | 5 +++-- pre_commit/store.py | 6 +++--- tests/commands/clean_test.py | 21 +++++++++++++++++++-- tests/error_handler_test.py | 9 ++++++--- tests/main_test.py | 4 +++- tests/store_test.py | 10 +++++++++- 9 files changed, 50 insertions(+), 17 deletions(-) diff --git a/.travis.yml b/.travis.yml index e84d8cc1e..8f91d702c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,4 +28,4 @@ after_success: coveralls cache: directories: - $HOME/.cache/pip - - $HOME/.pre-commit + - $HOME/.cache/pre-commit diff --git a/appveyor.yml b/appveyor.yml index 013e1421a..ddb9af3cd 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -23,4 +23,4 @@ test_script: tox cache: - '%LOCALAPPDATA%\pip\cache' - - '%USERPROFILE%\.pre-commit' + - '%USERPROFILE%\.cache\pre-commit' diff --git a/pre_commit/commands/clean.py b/pre_commit/commands/clean.py index 8cea6fc14..75d0acc03 100644 --- a/pre_commit/commands/clean.py +++ b/pre_commit/commands/clean.py @@ -8,7 +8,9 @@ def clean(runner): - if os.path.exists(runner.store.directory): - rmtree(runner.store.directory) - output.write_line('Cleaned {}.'.format(runner.store.directory)) + legacy_path = os.path.expanduser('~/.pre-commit') + for directory in (runner.store.directory, legacy_path): + if os.path.exists(directory): + rmtree(directory) + output.write_line('Cleaned {}.'.format(directory)) return 0 diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index b248f9344..76662e972 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -28,10 +28,11 @@ def _log_and_exit(msg, exc, formatted): _to_bytes(exc), b'\n', )) output.write(error_msg) - output.write_line('Check the log at ~/.pre-commit/pre-commit.log') store = Store() store.require_created() - with open(os.path.join(store.directory, 'pre-commit.log'), 'wb') as log: + log_path = os.path.join(store.directory, 'pre-commit.log') + output.write_line('Check the log at {}'.format(log_path)) + with open(log_path, 'wb') as log: output.write(error_msg, stream=log) output.write_line(formatted, stream=log) raise SystemExit(1) diff --git a/pre_commit/store.py b/pre_commit/store.py index 263b315bd..3262bda2b 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -29,9 +29,9 @@ def _get_default_directory(): `Store.get_default_directory` can be mocked in tests and `_get_default_directory` can be tested. """ - return os.environ.get( - 'PRE_COMMIT_HOME', - os.path.join(os.path.expanduser('~'), '.pre-commit'), + return os.environ.get('PRE_COMMIT_HOME') or os.path.join( + os.environ.get('XDG_CACHE_HOME') or os.path.expanduser('~/.cache'), + 'pre-commit', ) diff --git a/tests/commands/clean_test.py b/tests/commands/clean_test.py index bdbdc9987..fddd444d9 100644 --- a/tests/commands/clean_test.py +++ b/tests/commands/clean_test.py @@ -2,18 +2,35 @@ import os.path +import mock +import pytest + from pre_commit.commands.clean import clean from pre_commit.util import rmtree -def test_clean(runner_with_mocked_store): +@pytest.fixture(autouse=True) +def fake_old_dir(tempdir_factory): + fake_old_dir = tempdir_factory.get() + + def _expanduser(path, *args, **kwargs): + assert path == '~/.pre-commit' + return fake_old_dir + + with mock.patch.object(os.path, 'expanduser', side_effect=_expanduser): + yield fake_old_dir + + +def test_clean(runner_with_mocked_store, fake_old_dir): + assert os.path.exists(fake_old_dir) assert os.path.exists(runner_with_mocked_store.store.directory) clean(runner_with_mocked_store) + assert not os.path.exists(fake_old_dir) assert not os.path.exists(runner_with_mocked_store.store.directory) def test_clean_empty(runner_with_mocked_store): - """Make sure clean succeeds when we the directory doesn't exist.""" + """Make sure clean succeeds when the directory doesn't exist.""" rmtree(runner_with_mocked_store.store.directory) assert not os.path.exists(runner_with_mocked_store.store.directory) clean(runner_with_mocked_store) diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index bdc54b6a0..d6eaf500b 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -81,12 +81,12 @@ def test_log_and_exit(cap_out, mock_out_store_directory): ) printed = cap_out.get() + log_file = os.path.join(mock_out_store_directory, 'pre-commit.log') assert printed == ( 'msg: FatalError: hai\n' - 'Check the log at ~/.pre-commit/pre-commit.log\n' + 'Check the log at {}\n'.format(log_file) ) - log_file = os.path.join(mock_out_store_directory, 'pre-commit.log') assert os.path.exists(log_file) contents = io.open(log_file).read() assert contents == ( @@ -102,6 +102,7 @@ def test_error_handler_non_ascii_exception(mock_out_store_directory): def test_error_handler_no_tty(tempdir_factory): + pre_commit_home = tempdir_factory.get() output = cmd_output_mocked_pre_commit_home( sys.executable, '-c', 'from __future__ import unicode_literals\n' @@ -110,8 +111,10 @@ def test_error_handler_no_tty(tempdir_factory): ' raise ValueError("\\u2603")\n', retcode=1, tempdir_factory=tempdir_factory, + pre_commit_home=pre_commit_home, ) + log_file = os.path.join(pre_commit_home, 'pre-commit.log') assert output[1].replace('\r', '') == ( 'An unexpected error has occurred: ValueError: ☃\n' - 'Check the log at ~/.pre-commit/pre-commit.log\n' + 'Check the log at {}\n'.format(log_file) ) diff --git a/tests/main_test.py b/tests/main_test.py index 4348b8ceb..29c9ea29c 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import argparse +import os.path import mock import pytest @@ -121,10 +122,11 @@ def test_expected_fatal_error_no_git_repo( with cwd(tempdir_factory.get()): with pytest.raises(SystemExit): main.main([]) + log_file = os.path.join(mock_out_store_directory, 'pre-commit.log') assert cap_out.get() == ( 'An error has occurred: FatalError: git failed. ' 'Is it installed, and are you in a Git repository directory?\n' - 'Check the log at ~/.pre-commit/pre-commit.log\n' + 'Check the log at {}\n'.format(log_file) ) diff --git a/tests/store_test.py b/tests/store_test.py index 106a4645e..718f24d0d 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -29,7 +29,15 @@ def test_our_session_fixture_works(): def test_get_default_directory_defaults_to_home(): # Not we use the module level one which is not mocked ret = _get_default_directory() - assert ret == os.path.join(os.path.expanduser('~'), '.pre-commit') + assert ret == os.path.join(os.path.expanduser('~/.cache'), 'pre-commit') + + +def test_adheres_to_xdg_specification(): + with mock.patch.dict( + os.environ, {'XDG_CACHE_HOME': '/tmp/fakehome'}, + ): + ret = _get_default_directory() + assert ret == os.path.join('/tmp/fakehome', 'pre-commit') def test_uses_environment_variable_when_present(): From 3e76cdaf2567d3c6c657f9b0009056a2e1cc3a1e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 5 Sep 2017 14:04:08 -0700 Subject: [PATCH 064/544] Enable map configurations (config v2). --- .pre-commit-config.yaml | 1 + pre_commit/clientlib.py | 18 +++++++++-- pre_commit/commands/autoupdate.py | 2 +- pre_commit/commands/sample_config.py | 3 +- pre_commit/runner.py | 10 +++--- pre_commit/schema.py | 8 +++-- testing/fixtures.py | 9 +++--- tests/clientlib_test.py | 46 +++++++++++++--------------- tests/commands/autoupdate_test.py | 6 ++-- tests/commands/run_test.py | 16 +++++----- tests/commands/sample_config_test.py | 3 +- 11 files changed, 70 insertions(+), 52 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1e529a7f2..363406421 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,4 @@ +repos: - repo: https://github.com/pre-commit/pre-commit-hooks.git sha: v0.9.1 hooks: diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 6da6db25d..e69359b00 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import argparse +import collections import functools from aspy.yaml import ordered_load @@ -125,7 +126,11 @@ def validate_manifest_main(argv=None): ensure_absent=True, ), ) -CONFIG_SCHEMA = schema.Array(CONFIG_REPO_DICT) +CONFIG_SCHEMA = schema.Map( + 'Config', None, + + schema.RequiredRecurse('repos', schema.Array(CONFIG_REPO_DICT)), +) def is_local_repo(repo_entry): @@ -136,10 +141,19 @@ class InvalidConfigError(FatalError): pass +def ordered_load_normalize_legacy_config(contents): + data = ordered_load(contents) + if isinstance(data, list): + # TODO: Once happy, issue a deprecation warning and instructions + return collections.OrderedDict([('repos', data)]) + else: + return data + + load_config = functools.partial( schema.load_from_filename, schema=CONFIG_SCHEMA, - load_strategy=ordered_load, + load_strategy=ordered_load_normalize_legacy_config, exc_tp=InvalidConfigError, ) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 1c79500d3..810e53f60 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -109,7 +109,7 @@ def autoupdate(runner, tags_only): input_configs = load_config(runner.config_file_path) - for repo_config in input_configs: + for repo_config in input_configs['repos']: if is_local_repo(repo_config): output_configs.append(repo_config) continue diff --git a/pre_commit/commands/sample_config.py b/pre_commit/commands/sample_config.py index b74e4271c..c8c3bf107 100644 --- a/pre_commit/commands/sample_config.py +++ b/pre_commit/commands/sample_config.py @@ -10,8 +10,9 @@ SAMPLE_CONFIG = '''\ # See http://pre-commit.com for more information # See http://pre-commit.com/hooks.html for more hooks +repos: - repo: https://github.com/pre-commit/pre-commit-hooks - sha: v0.9.1 + sha: v0.9.2 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/pre_commit/runner.py b/pre_commit/runner.py index 21707cb4c..346d60214 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -40,11 +40,11 @@ def config_file_path(self): @cached_property def repositories(self): """Returns a tuple of the configured repositories.""" - config = load_config(self.config_file_path) - repositories = tuple(Repository.create(x, self.store) for x in config) - for repository in repositories: - repository.require_installed() - return repositories + repos = load_config(self.config_file_path)['repos'] + repos = tuple(Repository.create(x, self.store) for x in repos) + for repo in repos: + repo.require_installed() + return repos def get_hook_path(self, hook_type): return os.path.join(self.git_dir, 'hooks', hook_type) diff --git a/pre_commit/schema.py b/pre_commit/schema.py index a911bb437..f033071fb 100644 --- a/pre_commit/schema.py +++ b/pre_commit/schema.py @@ -142,9 +142,13 @@ def check(self, v): raise ValidationError('Expected a {} map but got a {}'.format( self.object_name, type(v).__name__, )) - with validate_context('At {}({}={!r})'.format( + if self.id_key is None: + context = 'At {}()'.format(self.object_name) + else: + context = 'At {}({}={!r})'.format( self.object_name, self.id_key, v.get(self.id_key, MISSING), - )): + ) + with validate_context(context): for item in self.items: item.check(v) diff --git a/testing/fixtures.py b/testing/fixtures.py index ac7950f11..1c61b2b1b 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -92,8 +92,9 @@ def make_config_from_repo(repo_path, sha=None, hooks=None, check=True): )) if check: - wrapped = validate([config], CONFIG_SCHEMA) - config, = apply_defaults(wrapped, CONFIG_SCHEMA) + wrapped = validate({'repos': [config]}, CONFIG_SCHEMA) + wrapped = apply_defaults(wrapped, CONFIG_SCHEMA) + config, = wrapped['repos'] return config else: return config @@ -106,9 +107,9 @@ def read_config(directory, config_file=C.CONFIG_FILE): def write_config(directory, config, config_file=C.CONFIG_FILE): - if type(config) is not list: + if type(config) is not list and 'repos' not in config: assert type(config) is OrderedDict - config = [config] + config = {'repos': [config]} with io.open(os.path.join(directory, config_file), 'w') as outfile: outfile.write(ordered_dump(config, **C.YAML_DUMP_KWARGS)) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 6c04648c7..8e85e6c44 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -60,15 +60,15 @@ def test_validate_config_main(args, expected_output): ('config_obj', 'expected'), ( ([], False), ( - [{ + {'repos': [{ 'repo': 'git@github.com:pre-commit/pre-commit-hooks', 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', 'hooks': [{'id': 'pyflakes', 'files': '\\.py$'}], - }], + }]}, True, ), ( - [{ + {'repos': [{ 'repo': 'git@github.com:pre-commit/pre-commit-hooks', 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', 'hooks': [ @@ -78,11 +78,11 @@ def test_validate_config_main(args, expected_output): 'args': ['foo', 'bar', 'baz'], }, ], - }], + }]}, True, ), ( - [{ + {'repos': [{ 'repo': 'git@github.com:pre-commit/pre-commit-hooks', 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', 'hooks': [ @@ -94,7 +94,7 @@ def test_validate_config_main(args, expected_output): 'args': ['foo', 'bar', 'baz'], }, ], - }], + }]}, False, ), ), @@ -104,29 +104,25 @@ def test_config_valid(config_obj, expected): assert ret is expected -@pytest.mark.parametrize( - 'config_obj', ( - [{ - 'repo': 'local', - 'sha': 'foo', - 'hooks': [{ - 'id': 'do_not_commit', - 'name': 'Block if "DO NOT COMMIT" is found', - 'entry': 'DO NOT COMMIT', - 'language': 'pcre', - 'files': '^(.*)$', - }], +def test_config_with_local_hooks_definition_fails(): + config_obj = {'repos': [{ + 'repo': 'local', + 'sha': 'foo', + 'hooks': [{ + 'id': 'do_not_commit', + 'name': 'Block if "DO NOT COMMIT" is found', + 'entry': 'DO NOT COMMIT', + 'language': 'pcre', + 'files': '^(.*)$', }], - ), -) -def test_config_with_local_hooks_definition_fails(config_obj): + }]} with pytest.raises(schema.ValidationError): schema.validate(config_obj, CONFIG_SCHEMA) @pytest.mark.parametrize( 'config_obj', ( - [{ + {'repos': [{ 'repo': 'local', 'hooks': [{ 'id': 'arg-per-line', @@ -136,8 +132,8 @@ def test_config_with_local_hooks_definition_fails(config_obj): 'files': '', 'args': ['hello', 'world'], }], - }], - [{ + }]}, + {'repos': [{ 'repo': 'local', 'hooks': [{ 'id': 'arg-per-line', @@ -147,7 +143,7 @@ def test_config_with_local_hooks_definition_fails(config_obj): 'files': '', 'args': ['hello', 'world'], }], - }], + }]}, ), ) def test_config_with_local_hooks_definition_passes(config_obj): diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 1920610a1..2d3530002 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -274,7 +274,7 @@ def test_autoupdate_local_hooks(tempdir_factory): assert autoupdate(runner, tags_only=False) == 0 new_config_writen = load_config(runner.config_file_path) assert len(new_config_writen) == 1 - assert new_config_writen[0] == config + assert new_config_writen['repos'][0] == config def test_autoupdate_local_hooks_with_out_of_date_repo( @@ -289,5 +289,5 @@ def test_autoupdate_local_hooks_with_out_of_date_repo( runner = Runner('.', C.CONFIG_FILE) assert autoupdate(runner, tags_only=False) == 0 new_config_writen = load_config(runner.config_file_path) - assert len(new_config_writen) == 2 - assert new_config_writen[0] == local_config + assert len(new_config_writen['repos']) == 2 + assert new_config_writen['repos'][0] == local_config diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index b544eb75f..39d3ac0ba 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -272,7 +272,7 @@ def test_always_run( cap_out, repo_with_passing_hook, mock_out_store_directory, ): with modify_config() as config: - config[0]['hooks'][0]['always_run'] = True + config['repos'][0]['hooks'][0]['always_run'] = True _test_run( cap_out, repo_with_passing_hook, @@ -288,7 +288,7 @@ def test_always_run_alt_config( ): repo_root = '.' config = read_config(repo_root) - config[0]['hooks'][0]['always_run'] = True + config['repos'][0]['hooks'][0]['always_run'] = True alt_config_file = 'alternate_config.yaml' add_config_to_repo(repo_root, config, config_file=alt_config_file) @@ -428,7 +428,7 @@ def test_multiple_hooks_same_id( with cwd(repo_with_passing_hook): # Add bash hook on there again with modify_config() as config: - config[0]['hooks'].append({'id': 'bash_hook'}) + config['repos'][0]['hooks'].append({'id': 'bash_hook'}) stage_a_file() ret, output = _do_run(cap_out, repo_with_passing_hook, _get_opts()) @@ -455,7 +455,7 @@ def test_stdout_write_bug_py26( ): with cwd(repo_with_failing_hook): with modify_config() as config: - config[0]['hooks'][0]['args'] = ['☃'] + config['repos'][0]['hooks'][0]['args'] = ['☃'] stage_a_file() install(Runner(repo_with_failing_hook, C.CONFIG_FILE)) @@ -505,7 +505,7 @@ def test_lots_of_files(mock_out_store_directory, tempdir_factory): with cwd(git_path): # Override files so we run against them with modify_config() as config: - config[0]['hooks'][0]['files'] = '' + config['repos'][0]['hooks'][0]['files'] = '' # Write a crap ton of files for i in range(400): @@ -660,7 +660,7 @@ def test_local_hook_fails( def modified_config_repo(repo_with_passing_hook): with modify_config(repo_with_passing_hook, commit=False) as config: # Some minor modification - config[0]['hooks'][0]['files'] = '' + config['repos'][0]['hooks'][0]['files'] = '' yield repo_with_passing_hook @@ -721,8 +721,8 @@ def test_pass_filenames( expected_out, ): with modify_config() as config: - config[0]['hooks'][0]['pass_filenames'] = pass_filenames - config[0]['hooks'][0]['args'] = hook_args + config['repos'][0]['hooks'][0]['pass_filenames'] = pass_filenames + config['repos'][0]['hooks'][0]['args'] = hook_args stage_a_file() ret, printed = _do_run( cap_out, repo_with_passing_hook, _get_opts(verbose=True), diff --git a/tests/commands/sample_config_test.py b/tests/commands/sample_config_test.py index 122d7bfc5..9d74a011b 100644 --- a/tests/commands/sample_config_test.py +++ b/tests/commands/sample_config_test.py @@ -11,8 +11,9 @@ def test_sample_config(capsys): assert out == '''\ # See http://pre-commit.com for more information # See http://pre-commit.com/hooks.html for more hooks +repos: - repo: https://github.com/pre-commit/pre-commit-hooks - sha: v0.9.1 + sha: v0.9.2 hooks: - id: trailing-whitespace - id: end-of-file-fixer From 8f5675d8138c9b2a4d9e6663aca9b2e6eda0c838 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 5 Sep 2017 16:32:04 -0700 Subject: [PATCH 065/544] Implement `pre-commit migrate-config` --- pre_commit/commands/autoupdate.py | 20 +++-- pre_commit/commands/migrate_config.py | 52 +++++++++++ pre_commit/main.py | 10 +++ tests/commands/autoupdate_test.py | 32 ++++++- tests/commands/migrate_config_test.py | 120 ++++++++++++++++++++++++++ tests/main_test.py | 4 +- 6 files changed, 226 insertions(+), 12 deletions(-) create mode 100644 pre_commit/commands/migrate_config.py create mode 100644 tests/commands/migrate_config_test.py diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 810e53f60..5b163c585 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -11,6 +11,7 @@ from pre_commit import output from pre_commit.clientlib import is_local_repo from pre_commit.clientlib import load_config +from pre_commit.commands.migrate_config import migrate_config from pre_commit.repository import Repository from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output @@ -104,21 +105,22 @@ def _write_new_config_file(path, output): def autoupdate(runner, tags_only): """Auto-update the pre-commit config to the latest versions of repos.""" retv = 0 - output_configs = [] + retv |= migrate_config(runner, quiet=True) + output_repos = [] changed = False - input_configs = load_config(runner.config_file_path) + input_config = load_config(runner.config_file_path) - for repo_config in input_configs['repos']: + for repo_config in input_config['repos']: if is_local_repo(repo_config): - output_configs.append(repo_config) + output_repos.append(repo_config) continue output.write('Updating {}...'.format(repo_config['repo'])) try: new_repo_config = _update_repo(repo_config, runner, tags_only) except RepositoryCannotBeUpdatedError as error: output.write_line(error.args[0]) - output_configs.append(repo_config) + output_repos.append(repo_config) retv = 1 continue @@ -127,12 +129,14 @@ def autoupdate(runner, tags_only): output.write_line('updating {} -> {}.'.format( repo_config['sha'], new_repo_config['sha'], )) - output_configs.append(new_repo_config) + output_repos.append(new_repo_config) else: output.write_line('already up to date.') - output_configs.append(repo_config) + output_repos.append(repo_config) if changed: - _write_new_config_file(runner.config_file_path, output_configs) + output_config = input_config.copy() + output_config['repos'] = output_repos + _write_new_config_file(runner.config_file_path, output_config) return retv diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py new file mode 100644 index 000000000..3c0b125aa --- /dev/null +++ b/pre_commit/commands/migrate_config.py @@ -0,0 +1,52 @@ +from __future__ import print_function +from __future__ import unicode_literals + +import io + +import yaml +from aspy.yaml import ordered_load + + +def _indent(s): + lines = s.splitlines(True) + return ''.join(' ' * 4 + line if line.strip() else line for line in lines) + + +def _is_header_line(line): + return (line.startswith(('#', '---')) or not line.strip()) + + +def migrate_config(runner, quiet=False): + retv = 0 + + with io.open(runner.config_file_path) as f: + contents = f.read() + + # Find the first non-header line + lines = contents.splitlines(True) + i = 0 + while _is_header_line(lines[i]): + i += 1 + + header = ''.join(lines[:i]) + rest = ''.join(lines[i:]) + + if isinstance(ordered_load(contents), list): + # If they are using the "default" flow style of yaml, this operation + # will yield a valid configuration + try: + trial_contents = header + 'repos:\n' + rest + yaml.load(trial_contents) + contents = trial_contents + except yaml.YAMLError: + contents = header + 'repos:\n' + _indent(rest) + + with io.open(runner.config_file_path, 'w') as f: + f.write(contents) + + print('Configuration has been migrated.') + retv = 1 + elif not quiet: + print('Configuration is already migrated.') + + return retv diff --git a/pre_commit/main.py b/pre_commit/main.py index 0b00a86ee..9167ee230 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -14,6 +14,7 @@ from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import install_hooks from pre_commit.commands.install_uninstall import uninstall +from pre_commit.commands.migrate_config import migrate_config from pre_commit.commands.run import run from pre_commit.commands.sample_config import sample_config from pre_commit.error_handler import error_handler @@ -131,6 +132,13 @@ def main(argv=None): ), ) + migrate_config_parser = subparsers.add_parser( + 'migrate-config', + help='Migrate list configuration to new map configuration.', + ) + _add_color_option(migrate_config_parser) + _add_config_option(migrate_config_parser) + run_parser = subparsers.add_parser('run', help='Run hooks.') _add_color_option(run_parser) _add_config_option(run_parser) @@ -217,6 +225,8 @@ def main(argv=None): if args.tags_only: logger.warning('--tags-only is the default') return autoupdate(runner, tags_only=not args.bleeding_edge) + elif args.command == 'migrate-config': + return migrate_config(runner) elif args.command == 'run': return run(runner, args) elif args.command == 'sample-config': diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 2d3530002..3be94cd1d 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -128,6 +128,7 @@ def test_does_not_reformat( out_of_date_repo, mock_out_store_directory, in_tmpdir, ): fmt = ( + 'repos:\n' '- repo: {}\n' ' sha: {} # definitely the version I want!\n' ' hooks:\n' @@ -153,7 +154,7 @@ def test_loses_formatting_when_not_detectable( is abandoned. """ config = ( - '[\n' + 'repos: [\n' ' {{\n' ' repo: {}, sha: {},\n' ' hooks: [\n' @@ -171,6 +172,7 @@ def test_loses_formatting_when_not_detectable( autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) after = open(C.CONFIG_FILE).read() expected = ( + 'repos:\n' '- repo: {}\n' ' sha: {}\n' ' hooks:\n' @@ -284,10 +286,36 @@ def test_autoupdate_local_hooks_with_out_of_date_repo( out_of_date_repo.path, sha=out_of_date_repo.original_sha, check=False, ) local_config = config_with_local_hooks() - config = [local_config, stale_config] + config = {'repos': [local_config, stale_config]} write_config('.', config) runner = Runner('.', C.CONFIG_FILE) assert autoupdate(runner, tags_only=False) == 0 new_config_writen = load_config(runner.config_file_path) assert len(new_config_writen['repos']) == 2 assert new_config_writen['repos'][0] == local_config + + +def test_updates_old_format_to_new_format(tmpdir, capsys): + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write( + '- repo: local\n' + ' hooks:\n' + ' - id: foo\n' + ' name: foo\n' + ' entry: ./bin/foo.sh\n' + ' language: script\n', + ) + ret = autoupdate(Runner(tmpdir.strpath, C.CONFIG_FILE), tags_only=True) + assert ret == 1 + contents = cfg.read() + assert contents == ( + 'repos:\n' + '- repo: local\n' + ' hooks:\n' + ' - id: foo\n' + ' name: foo\n' + ' entry: ./bin/foo.sh\n' + ' language: script\n' + ) + out, _ = capsys.readouterr() + assert out == 'Configuration has been migrated.\n' diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py new file mode 100644 index 000000000..c406f4797 --- /dev/null +++ b/tests/commands/migrate_config_test.py @@ -0,0 +1,120 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import pytest + +import pre_commit.constants as C +from pre_commit.commands.migrate_config import _indent +from pre_commit.commands.migrate_config import migrate_config +from pre_commit.runner import Runner + + +@pytest.mark.parametrize( + ('s', 'expected'), + ( + ('', ''), + ('a', ' a'), + ('foo\nbar', ' foo\n bar'), + ('foo\n\nbar\n', ' foo\n\n bar\n'), + ('\n\n\n', '\n\n\n'), + ), +) +def test_indent(s, expected): + assert _indent(s) == expected + + +def test_migrate_config_normal_format(tmpdir, capsys): + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write( + '- repo: local\n' + ' hooks:\n' + ' - id: foo\n' + ' name: foo\n' + ' entry: ./bin/foo.sh\n' + ' language: script\n', + ) + assert migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) == 1 + out, _ = capsys.readouterr() + assert out == 'Configuration has been migrated.\n' + contents = cfg.read() + assert contents == ( + 'repos:\n' + '- repo: local\n' + ' hooks:\n' + ' - id: foo\n' + ' name: foo\n' + ' entry: ./bin/foo.sh\n' + ' language: script\n' + ) + + +def test_migrate_config_document_marker(tmpdir): + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write( + '# comment\n' + '\n' + '---\n' + '- repo: local\n' + ' hooks:\n' + ' - id: foo\n' + ' name: foo\n' + ' entry: ./bin/foo.sh\n' + ' language: script\n', + ) + assert migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) == 1 + contents = cfg.read() + assert contents == ( + '# comment\n' + '\n' + '---\n' + 'repos:\n' + '- repo: local\n' + ' hooks:\n' + ' - id: foo\n' + ' name: foo\n' + ' entry: ./bin/foo.sh\n' + ' language: script\n' + ) + + +def test_migrate_config_list_literal(tmpdir): + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write( + '[{\n' + ' repo: local,\n' + ' hooks: [{\n' + ' id: foo, name: foo, entry: ./bin/foo.sh,\n' + ' language: script,\n' + ' }]\n' + '}]', + ) + assert migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) == 1 + contents = cfg.read() + assert contents == ( + 'repos:\n' + ' [{\n' + ' repo: local,\n' + ' hooks: [{\n' + ' id: foo, name: foo, entry: ./bin/foo.sh,\n' + ' language: script,\n' + ' }]\n' + ' }]' + ) + + +def test_already_migrated_configuration_noop(tmpdir, capsys): + contents = ( + 'repos:\n' + '- repo: local\n' + ' hooks:\n' + ' - id: foo\n' + ' name: foo\n' + ' entry: ./bin/foo.sh\n' + ' language: script\n' + ) + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write(contents) + assert not migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) + out, _ = capsys.readouterr() + assert out == 'Configuration is already migrated.\n' + assert cfg.read() == contents diff --git a/tests/main_test.py b/tests/main_test.py index 4348b8ceb..bbc812826 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -12,8 +12,8 @@ FNS = ( - 'autoupdate', 'clean', 'install', 'install_hooks', 'run', 'sample_config', - 'uninstall', + 'autoupdate', 'clean', 'install', 'install_hooks', 'migrate_config', 'run', + 'sample_config', 'uninstall', ) CMDS = tuple(fn.replace('_', '-') for fn in FNS) From 3f1704ff256d1abba7a505f1b81919b106045449 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 6 Sep 2017 10:13:02 -0700 Subject: [PATCH 066/544] v0.18.3 --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c12396a6b..80f222882 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +0.18.3 +====== +- Allow --config to affect `pre-commit install` +- Tweak not found error message during `pre-push` / `commit-msg` +- Improve node support when running under cygwin. + 0.18.2 ====== - Fix `--all-files`, detection of staged files, detection of manually edited diff --git a/setup.py b/setup.py index e6ea9b9dc..00d31e805 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.18.2', + version='0.18.3', author='Anthony Sottile', author_email='asottile@umich.edu', From 54e71c1babed89a20fc3b4e26572c31a2cd74b6e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 7 Sep 2017 08:50:02 -0700 Subject: [PATCH 067/544] v1.0.0 --- CHANGELOG.md | 42 ++++++++++++++++++++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80f222882..5fcdcb581 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,45 @@ +1.0.0 +===== +pre-commit will now be following [semver](http://semver.org/). Thanks to all +of the [contributors](https://github.com/pre-commit/pre-commit/graphs/contributors) +that have helped us get this far! + +### Features + +- pre-commit's cache directory has moved from `~/.pre-commit` to + `$XDG_CACHE_HOME/pre-commit` (usually `~/.cache/pre-commit`). + - `pre-commit clean` now cleans up both the old and new directory. + - If you were caching this directory in CI, you'll want to adjust the + location. + - #562 issue by @nagromc. + - #602 PR by @asottile. +- A new configuration format for `.pre-commit-config.yaml` is introduced which + will enable future development. + - The new format has a top-level map instead of a top-level list. The + new format puts the hook repositories in a `hooks` key. + - Old list-based configurations will continue to be supported. + - A command `pre-commit migrate-config` has been introduced to "upgrade" + the configuration format to the new map-based configuration. + - `pre-commit autoupdate` now automatically calls `migrate-config`. + - In a later release, list-based configurations will issue a deprecation + warning. + - An example diff for upgrading a configuration: + + ```diff + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + sha: v0.9.2 + hooks: + ``` + - #414 issue by @asottile. + - #610 PR by @asottile. + +### Updating + +- Run `pre-commit migrate-config` to convert `.pre-commit-config.yaml` to the + new map format. +- Update any references from `~/.pre-commit` to `~/.cache/pre-commit`. + 0.18.3 ====== - Allow --config to affect `pre-commit install` diff --git a/setup.py b/setup.py index 00d31e805..ab1de4872 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.18.3', + version='1.0.0', author='Anthony Sottile', author_email='asottile@umich.edu', From 04018ad4e744d0c0df7f152a64799ffe9f40078a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 7 Sep 2017 08:57:42 -0700 Subject: [PATCH 068/544] Fix typo in CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fcdcb581..cd172e6fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ that have helped us get this far! - A new configuration format for `.pre-commit-config.yaml` is introduced which will enable future development. - The new format has a top-level map instead of a top-level list. The - new format puts the hook repositories in a `hooks` key. + new format puts the hook repositories in a `repos` key. - Old list-based configurations will continue to be supported. - A command `pre-commit migrate-config` has been introduced to "upgrade" the configuration format to the new map-based configuration. From a78f5d5c247cd2eae944a480cf8cf8b5795d271b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 7 Sep 2017 09:23:36 -0700 Subject: [PATCH 069/544] pre-commit migrate-config should not return nonzero when successful --- pre_commit/commands/autoupdate.py | 2 +- pre_commit/commands/migrate_config.py | 5 ----- tests/commands/autoupdate_test.py | 2 +- tests/commands/migrate_config_test.py | 6 +++--- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 5b163c585..17588cc3a 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -104,8 +104,8 @@ def _write_new_config_file(path, output): def autoupdate(runner, tags_only): """Auto-update the pre-commit config to the latest versions of repos.""" + migrate_config(runner, quiet=True) retv = 0 - retv |= migrate_config(runner, quiet=True) output_repos = [] changed = False diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index 3c0b125aa..50f0c2da0 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -17,8 +17,6 @@ def _is_header_line(line): def migrate_config(runner, quiet=False): - retv = 0 - with io.open(runner.config_file_path) as f: contents = f.read() @@ -45,8 +43,5 @@ def migrate_config(runner, quiet=False): f.write(contents) print('Configuration has been migrated.') - retv = 1 elif not quiet: print('Configuration is already migrated.') - - return retv diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 3be94cd1d..7fb21b9d2 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -306,7 +306,7 @@ def test_updates_old_format_to_new_format(tmpdir, capsys): ' language: script\n', ) ret = autoupdate(Runner(tmpdir.strpath, C.CONFIG_FILE), tags_only=True) - assert ret == 1 + assert ret == 0 contents = cfg.read() assert contents == ( 'repos:\n' diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index c406f4797..7b43098bf 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -33,7 +33,7 @@ def test_migrate_config_normal_format(tmpdir, capsys): ' entry: ./bin/foo.sh\n' ' language: script\n', ) - assert migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) == 1 + assert not migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) out, _ = capsys.readouterr() assert out == 'Configuration has been migrated.\n' contents = cfg.read() @@ -61,7 +61,7 @@ def test_migrate_config_document_marker(tmpdir): ' entry: ./bin/foo.sh\n' ' language: script\n', ) - assert migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) == 1 + assert not migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) contents = cfg.read() assert contents == ( '# comment\n' @@ -88,7 +88,7 @@ def test_migrate_config_list_literal(tmpdir): ' }]\n' '}]', ) - assert migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) == 1 + assert not migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) contents = cfg.read() assert contents == ( 'repos:\n' From e3ab8902692e896da9ded42bd4d76ea4e1de359d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 7 Sep 2017 09:40:06 -0700 Subject: [PATCH 070/544] Work around travis-ci/travis-ci#8363 --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 8f91d702c..aebcb2328 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,8 @@ matrix: install: pip install coveralls tox script: tox before_install: + # work around https://github.com/travis-ci/travis-ci/issues/8363 + - pyenv global system 3.5 - git --version - | if [ "$LATEST_GIT" = "1" ]; then From 94dde266033b0d2eb1ef2ce4facd05a87a2682b9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 7 Sep 2017 09:56:51 -0700 Subject: [PATCH 071/544] v1.0.1 --- CHANGELOG.md | 9 +++++++++ setup.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd172e6fc..229e2d11b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +1.0.1 +===== + +## Fixes +- Fix a regression in the return code of `pre-commit autoupdate` + - `pre-commit migrate-config` and `pre-commit autoupdate` return 0 when + successful. + - #614 PR by @asottile. + 1.0.0 ===== pre-commit will now be following [semver](http://semver.org/). Thanks to all diff --git a/setup.py b/setup.py index ab1de4872..68c6a4c48 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.0.0', + version='1.0.1', author='Anthony Sottile', author_email='asottile@umich.edu', From 898a3ea1bb7dc2a3a0a65f8b2019408691c557ff Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 8 Sep 2017 13:19:00 -0700 Subject: [PATCH 072/544] Implement `fail_fast`. --- pre_commit/clientlib.py | 1 + pre_commit/commands/run.py | 6 ++++-- pre_commit/runner.py | 6 +++++- tests/commands/run_test.py | 15 +++++++++++++++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index e69359b00..7fb49d789 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -130,6 +130,7 @@ def validate_manifest_main(argv=None): 'Config', None, schema.RequiredRecurse('repos', schema.Array(CONFIG_REPO_DICT)), + schema.Optional('fail_fast', schema.check_bool, False), ) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 99232585e..505bb54db 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -169,13 +169,15 @@ def _compute_cols(hooks, verbose): return max(cols, 80) -def _run_hooks(repo_hooks, args, environ): +def _run_hooks(config, repo_hooks, args, environ): """Actually run the hooks.""" skips = _get_skips(environ) cols = _compute_cols([hook for _, hook in repo_hooks], args.verbose) retval = 0 for repo, hook in repo_hooks: retval |= _run_single_hook(hook, repo, args, skips, cols) + if retval and config['fail_fast']: + break if ( retval and args.show_diff_on_failure and @@ -251,4 +253,4 @@ def run(runner, args, environ=os.environ): if not hook['stages'] or args.hook_stage in hook['stages'] ] - return _run_hooks(repo_hooks, args, environ) + return _run_hooks(runner.config, repo_hooks, args, environ) diff --git a/pre_commit/runner.py b/pre_commit/runner.py index 346d60214..d853868a4 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -37,10 +37,14 @@ def git_dir(self): def config_file_path(self): return os.path.join(self.git_root, self.config_file) + @cached_property + def config(self): + return load_config(self.config_file_path) + @cached_property def repositories(self): """Returns a tuple of the configured repositories.""" - repos = load_config(self.config_file_path)['repos'] + repos = self.config['repos'] repos = tuple(Repository.create(x, self.store) for x in repos) for repo in repos: repo.require_installed() diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 39d3ac0ba..53e098b05 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -729,3 +729,18 @@ def test_pass_filenames( ) assert expected_out + b'\nHello World' in printed assert (b'foo.py' in printed) == pass_filenames + + +def test_fail_fast( + cap_out, repo_with_failing_hook, mock_out_store_directory, +): + with cwd(repo_with_failing_hook): + with modify_config() as config: + # More than one hook + config['fail_fast'] = True + config['repos'][0]['hooks'] *= 2 + stage_a_file() + + ret, printed = _do_run(cap_out, repo_with_failing_hook, _get_opts()) + # it should have only run one hook + assert printed.count(b'Failing hook') == 1 From 72e3989350246a759cec8dea6b0b5829025c08cf Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 8 Sep 2017 14:22:36 -0700 Subject: [PATCH 073/544] Revert "Remove remove_defaults -- it wasn't doing anything" This reverts commit ee392275f308032dc47ec0dea9d19c92b89d5996. --- pre_commit/commands/autoupdate.py | 3 +++ pre_commit/schema.py | 27 ++++++++++++++++++++++ tests/schema_test.py | 38 +++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 17588cc3a..844c60172 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -9,10 +9,12 @@ import pre_commit.constants as C from pre_commit import output +from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import is_local_repo from pre_commit.clientlib import load_config from pre_commit.commands.migrate_config import migrate_config from pre_commit.repository import Repository +from pre_commit.schema import remove_defaults from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cwd @@ -71,6 +73,7 @@ def _update_repo(repo_config, runner, tags_only): def _write_new_config_file(path, output): original_contents = open(path).read() + output = remove_defaults(output, CONFIG_SCHEMA) new_contents = ordered_dump(output, **C.YAML_DUMP_KWARGS) lines = original_contents.splitlines(True) diff --git a/pre_commit/schema.py b/pre_commit/schema.py index f033071fb..e20f74ccb 100644 --- a/pre_commit/schema.py +++ b/pre_commit/schema.py @@ -64,6 +64,11 @@ def _apply_default_optional(self, dct): dct.setdefault(self.key, self.default) +def _remove_default_optional(self, dct): + if dct.get(self.key, MISSING) == self.default: + del dct[self.key] + + def _require_key(self, dct): if self.key not in dct: raise ValidationError('Missing required key: {}'.format(self.key)) @@ -85,6 +90,10 @@ def _apply_default_required_recurse(self, dct): dct[self.key] = apply_defaults(dct[self.key], self.schema) +def _remove_default_required_recurse(self, dct): + dct[self.key] = remove_defaults(dct[self.key], self.schema) + + def _check_conditional(self, dct): if dct.get(self.condition_key, MISSING) == self.condition_value: _check_required(self, dct) @@ -110,18 +119,22 @@ def _check_conditional(self, dct): Required = collections.namedtuple('Required', ('key', 'check_fn')) Required.check = _check_required Required.apply_default = _dct_noop +Required.remove_default = _dct_noop RequiredRecurse = collections.namedtuple('RequiredRecurse', ('key', 'schema')) RequiredRecurse.check = _check_required RequiredRecurse.check_fn = _check_fn_required_recurse RequiredRecurse.apply_default = _apply_default_required_recurse +RequiredRecurse.remove_default = _remove_default_required_recurse Optional = collections.namedtuple('Optional', ('key', 'check_fn', 'default')) Optional.check = _check_optional Optional.apply_default = _apply_default_optional +Optional.remove_default = _remove_default_optional OptionalNoDefault = collections.namedtuple( 'OptionalNoDefault', ('key', 'check_fn'), ) OptionalNoDefault.check = _check_optional OptionalNoDefault.apply_default = _dct_noop +OptionalNoDefault.remove_default = _dct_noop Conditional = collections.namedtuple( 'Conditional', ('key', 'check_fn', 'condition_key', 'condition_value', 'ensure_absent'), @@ -129,6 +142,7 @@ def _check_conditional(self, dct): Conditional.__new__.__defaults__ = (False,) Conditional.check = _check_conditional Conditional.apply_default = _dct_noop +Conditional.remove_default = _dct_noop class Map(collections.namedtuple('Map', ('object_name', 'id_key', 'items'))): @@ -158,6 +172,12 @@ def apply_defaults(self, v): item.apply_default(ret) return ret + def remove_defaults(self, v): + ret = v.copy() + for item in self.items: + item.remove_default(ret) + return ret + class Array(collections.namedtuple('Array', ('of',))): __slots__ = () @@ -174,6 +194,9 @@ def check(self, v): def apply_defaults(self, v): return [apply_defaults(val, self.of) for val in v] + def remove_defaults(self, v): + return [remove_defaults(val, self.of) for val in v] + class Not(object): def __init__(self, val): @@ -238,6 +261,10 @@ def apply_defaults(v, schema): return schema.apply_defaults(v) +def remove_defaults(v, schema): + return schema.remove_defaults(v) + + def load_from_filename(filename, schema, load_strategy, exc_tp): with reraise_as(exc_tp): if not os.path.exists(filename): diff --git a/tests/schema_test.py b/tests/schema_test.py index c133a9971..c2ecf0faa 100644 --- a/tests/schema_test.py +++ b/tests/schema_test.py @@ -21,6 +21,7 @@ from pre_commit.schema import Not from pre_commit.schema import Optional from pre_commit.schema import OptionalNoDefault +from pre_commit.schema import remove_defaults from pre_commit.schema import Required from pre_commit.schema import RequiredRecurse from pre_commit.schema import validate @@ -280,6 +281,37 @@ def test_apply_defaults_map_in_list(): assert ret == [{'key': False}] +def test_remove_defaults_copies_object(): + val = {'key': False} + ret = remove_defaults(val, map_optional) + assert ret is not val + + +def test_remove_defaults_removes_defaults(): + ret = remove_defaults({'key': False}, map_optional) + assert ret == {} + + +def test_remove_defaults_nothing_to_remove(): + ret = remove_defaults({}, map_optional) + assert ret == {} + + +def test_remove_defaults_does_not_change_non_default(): + ret = remove_defaults({'key': True}, map_optional) + assert ret == {'key': True} + + +def test_remove_defaults_map_in_list(): + ret = remove_defaults([{'key': False}], Array(map_optional)) + assert ret == [{}] + + +def test_remove_defaults_does_nothing_on_non_optional(): + ret = remove_defaults({'key': True}, map_required) + assert ret == {'key': True} + + nested_schema_required = Map( 'Repository', 'repo', Required('repo', check_any), @@ -310,6 +342,12 @@ def test_apply_defaults_nested(): assert ret == {'repo': 'repo1', 'hooks': [{'key': False}]} +def test_remove_defaults_nested(): + val = {'repo': 'repo1', 'hooks': [{'key': False}]} + ret = remove_defaults(val, nested_schema_optional) + assert ret == {'repo': 'repo1', 'hooks': [{}]} + + class Error(Exception): pass From a821172d9d90b6d451c08adc01d4e310651a84bc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 8 Sep 2017 14:28:23 -0700 Subject: [PATCH 074/544] Remove defaults before checking whether the intelligent rewrite was successful --- pre_commit/commands/autoupdate.py | 2 +- tests/commands/autoupdate_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 844c60172..4dce674f3 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -98,7 +98,7 @@ def _write_new_config_file(path, output): # If we failed to intelligently rewrite the sha lines, fall back to the # pretty-formatted yaml output to_write = ''.join(lines) - if ordered_load(to_write) != output: + if remove_defaults(ordered_load(to_write), CONFIG_SCHEMA) != output: to_write = new_contents with open(path, 'w') as f: diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 7fb21b9d2..2877c5b3d 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -275,7 +275,7 @@ def test_autoupdate_local_hooks(tempdir_factory): runner = Runner(path, C.CONFIG_FILE) assert autoupdate(runner, tags_only=False) == 0 new_config_writen = load_config(runner.config_file_path) - assert len(new_config_writen) == 1 + assert len(new_config_writen['repos']) == 1 assert new_config_writen['repos'][0] == config From 6af60158ec40bd15fabdd91f55a571ef3845645e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 8 Sep 2017 17:48:48 -0700 Subject: [PATCH 075/544] Refactor filename collection for hooks --- pre_commit/commands/run.py | 62 ++++++++++++++++++++++---------------- pre_commit/git.py | 29 ------------------ tests/commands/run_test.py | 44 +++++++++++++++++++++++++++ tests/git_test.py | 51 ------------------------------- 4 files changed, 80 insertions(+), 106 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 505bb54db..e260b6628 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -3,6 +3,7 @@ import logging import os +import re import subprocess import sys @@ -36,7 +37,19 @@ def _hook_msg_start(hook, verbose): ) -def filter_filenames_by_types(filenames, types, exclude_types): +def _filter_by_include_exclude(filenames, include, exclude): + include_re, exclude_re = re.compile(include), re.compile(exclude) + return { + filename for filename in filenames + if ( + include_re.search(filename) and + not exclude_re.search(filename) and + os.path.lexists(filename) + ) + } + + +def _filter_by_types(filenames, types, exclude_types): types, exclude_types = frozenset(types), frozenset(exclude_types) ret = [] for filename in filenames: @@ -46,34 +59,15 @@ def filter_filenames_by_types(filenames, types, exclude_types): return tuple(ret) -def get_filenames(args, include_expr, exclude_expr): - if args.origin and args.source: - getter = git.get_files_matching( - lambda: git.get_changed_files(args.origin, args.source), - ) - elif args.hook_stage == 'commit-msg': - def getter(*_): - return (args.commit_msg_filename,) - elif args.files: - getter = git.get_files_matching(lambda: args.files) - elif args.all_files: - getter = git.get_all_files_matching - elif git.is_in_merge_conflict(): - getter = git.get_conflicted_files_matching - else: - getter = git.get_staged_files_matching - return getter(include_expr, exclude_expr) - - SKIPPED = 'Skipped' NO_FILES = '(no files to check)' -def _run_single_hook(hook, repo, args, skips, cols): - filenames = get_filenames(args, hook['files'], hook['exclude']) - filenames = filter_filenames_by_types( - filenames, hook['types'], hook['exclude_types'], - ) +def _run_single_hook(filenames, hook, repo, args, skips, cols): + include, exclude = hook['files'], hook['exclude'] + filenames = _filter_by_include_exclude(filenames, include, exclude) + types, exclude_types = hook['types'], hook['exclude_types'] + filenames = _filter_by_types(filenames, types, exclude_types) if hook['id'] in skips: output.write(get_hook_message( _hook_msg_start(hook, args.verbose), @@ -169,13 +163,29 @@ def _compute_cols(hooks, verbose): return max(cols, 80) +def _all_filenames(args): + if args.origin and args.source: + return git.get_changed_files(args.origin, args.source) + elif args.hook_stage == 'commit-msg': + return (args.commit_msg_filename,) + elif args.files: + return args.files + elif args.all_files: + return git.get_all_files() + elif git.is_in_merge_conflict(): + return git.get_conflicted_files() + else: + return git.get_staged_files() + + def _run_hooks(config, repo_hooks, args, environ): """Actually run the hooks.""" skips = _get_skips(environ) cols = _compute_cols([hook for _, hook in repo_hooks], args.verbose) + filenames = _all_filenames(args) retval = 0 for repo, hook in repo_hooks: - retval |= _run_single_hook(hook, repo, args, skips, cols) + retval |= _run_single_hook(filenames, hook, repo, args, skips, cols) if retval and config['fail_fast']: break if ( diff --git a/pre_commit/git.py b/pre_commit/git.py index cdf807b58..1c3191e3a 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -1,15 +1,12 @@ from __future__ import unicode_literals -import functools import logging import os.path -import re import sys from pre_commit.errors import FatalError from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output -from pre_commit.util import memoize_by_cwd logger = logging.getLogger('pre_commit') @@ -63,7 +60,6 @@ def parse_merge_msg_for_conflicts(merge_msg): ] -@memoize_by_cwd def get_conflicted_files(): logger.info('Checking merge-conflict files only.') # Need to get the conflicted files from the MERGE_MSG because they could @@ -82,7 +78,6 @@ def get_conflicted_files(): return set(merge_conflict_filenames) | set(merge_diff_filenames) -@memoize_by_cwd def get_staged_files(): return zsplit(cmd_output( 'git', 'diff', '--staged', '--name-only', '--no-ext-diff', '-z', @@ -91,7 +86,6 @@ def get_staged_files(): )[1]) -@memoize_by_cwd def get_all_files(): return zsplit(cmd_output('git', 'ls-files', '-z')[1]) @@ -103,29 +97,6 @@ def get_changed_files(new, old): )[1]) -def get_files_matching(all_file_list_strategy): - @functools.wraps(all_file_list_strategy) - @memoize_by_cwd - def wrapper(include_expr, exclude_expr): - include_regex = re.compile(include_expr) - exclude_regex = re.compile(exclude_expr) - return { - filename - for filename in all_file_list_strategy() - if ( - include_regex.search(filename) and - not exclude_regex.search(filename) and - os.path.lexists(filename) - ) - } - return wrapper - - -get_staged_files_matching = get_files_matching(get_staged_files) -get_all_files_matching = get_files_matching(get_all_files) -get_conflicted_files_matching = get_files_matching(get_conflicted_files) - - def check_for_cygwin_mismatch(): """See https://github.com/pre-commit/pre-commit/issues/354""" if sys.platform in ('cygwin', 'win32'): # pragma: no cover (windows) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 53e098b05..46d2a7e14 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -12,6 +12,7 @@ import pre_commit.constants as C from pre_commit.commands.install_uninstall import install from pre_commit.commands.run import _compute_cols +from pre_commit.commands.run import _filter_by_include_exclude from pre_commit.commands.run import _get_skips from pre_commit.commands.run import _has_unmerged_paths from pre_commit.commands.run import run @@ -25,6 +26,7 @@ from testing.fixtures import modify_config from testing.fixtures import read_config from testing.util import cmd_output_mocked_pre_commit_home +from testing.util import xfailif_no_symlink @pytest.yield_fixture @@ -744,3 +746,45 @@ def test_fail_fast( ret, printed = _do_run(cap_out, repo_with_failing_hook, _get_opts()) # it should have only run one hook assert printed.count(b'Failing hook') == 1 + + +@pytest.fixture +def some_filenames(): + return ( + '.pre-commit-hooks.yaml', + 'pre_commit/main.py', + 'pre_commit/git.py', + 'im_a_file_that_doesnt_exist.py', + ) + + +def test_include_exclude_base_case(some_filenames): + ret = _filter_by_include_exclude(some_filenames, '', '^$') + assert ret == { + '.pre-commit-hooks.yaml', + 'pre_commit/main.py', + 'pre_commit/git.py', + } + + +@xfailif_no_symlink +def test_matches_broken_symlink(tmpdir): # pramga: no cover (non-windows) + with tmpdir.as_cwd(): + os.symlink('does-not-exist', 'link') + ret = _filter_by_include_exclude({'link'}, '', '^$') + assert ret == {'link'} + + +def test_include_exclude_total_match(some_filenames): + ret = _filter_by_include_exclude(some_filenames, r'^.*\.py$', '^$') + assert ret == {'pre_commit/main.py', 'pre_commit/git.py'} + + +def test_include_exclude_does_search_instead_of_match(some_filenames): + ret = _filter_by_include_exclude(some_filenames, r'\.yaml$', '^$') + assert ret == {'.pre-commit-hooks.yaml'} + + +def test_include_exclude_exclude_removes_files(some_filenames): + ret = _filter_by_include_exclude(some_filenames, '', r'\.py$') + assert ret == {'.pre-commit-hooks.yaml'} diff --git a/tests/git_test.py b/tests/git_test.py index 4fce5ab08..8417523fe 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -11,7 +11,6 @@ from pre_commit.util import cmd_output from pre_commit.util import cwd from testing.fixtures import git_dir -from testing.util import xfailif_no_symlink def test_get_root_at_root(tempdir_factory): @@ -66,56 +65,6 @@ def test_cherry_pick_conflict(in_merge_conflict): assert git.is_in_merge_conflict() is False -@pytest.fixture -def get_files_matching_func(): - def get_filenames(): - return ( - '.pre-commit-hooks.yaml', - 'pre_commit/main.py', - 'pre_commit/git.py', - 'im_a_file_that_doesnt_exist.py', - ) - - return git.get_files_matching(get_filenames) - - -def test_get_files_matching_base(get_files_matching_func): - ret = get_files_matching_func('', '^$') - assert ret == { - '.pre-commit-hooks.yaml', - 'pre_commit/main.py', - 'pre_commit/git.py', - } - - -@xfailif_no_symlink -def test_matches_broken_symlink(tmpdir): # pragma: no cover (non-windwos) - with tmpdir.as_cwd(): - os.symlink('does-not-exist', 'link') - func = git.get_files_matching(lambda: ('link',)) - assert func('', '^$') == {'link'} - - -def test_get_files_matching_total_match(get_files_matching_func): - ret = get_files_matching_func('^.*\\.py$', '^$') - assert ret == {'pre_commit/main.py', 'pre_commit/git.py'} - - -def test_does_search_instead_of_match(get_files_matching_func): - ret = get_files_matching_func('\\.yaml$', '^$') - assert ret == {'.pre-commit-hooks.yaml'} - - -def test_does_not_include_deleted_fileS(get_files_matching_func): - ret = get_files_matching_func('exist.py', '^$') - assert ret == set() - - -def test_exclude_removes_files(get_files_matching_func): - ret = get_files_matching_func('', '\\.py$') - assert ret == {'.pre-commit-hooks.yaml'} - - def resolve_conflict(): with open('conflict_file', 'w') as conflicted_file: conflicted_file.write('herp\nderp\n') From ecdc22ce80da4a996ff08f3bbe5a779a9ac90ec1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 9 Sep 2017 22:02:01 -0700 Subject: [PATCH 076/544] Implement global exclude --- pre_commit/clientlib.py | 1 + pre_commit/commands/run.py | 1 + tests/commands/run_test.py | 15 +++++++++++++++ 3 files changed, 17 insertions(+) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 7fb49d789..c04cf333e 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -130,6 +130,7 @@ def validate_manifest_main(argv=None): 'Config', None, schema.RequiredRecurse('repos', schema.Array(CONFIG_REPO_DICT)), + schema.Optional('exclude', schema.check_regex, '^$'), schema.Optional('fail_fast', schema.check_bool, False), ) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index e260b6628..6f6954874 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -183,6 +183,7 @@ def _run_hooks(config, repo_hooks, args, environ): skips = _get_skips(environ) cols = _compute_cols([hook for _, hook in repo_hooks], args.verbose) filenames = _all_filenames(args) + filenames = _filter_by_include_exclude(filenames, '', config['exclude']) retval = 0 for repo, hook in repo_hooks: retval |= _run_single_hook(filenames, hook, repo, args, skips, cols) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 46d2a7e14..51e4eac91 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -183,6 +183,21 @@ def test_exclude_types_hook_repository( assert b'exe' not in printed +def test_global_exclude(cap_out, tempdir_factory, mock_out_store_directory): + git_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + with cwd(git_path): + with modify_config() as config: + config['exclude'] = '^foo.py$' + open('foo.py', 'a').close() + open('bar.py', 'a').close() + cmd_output('git', 'add', '.') + ret, printed = _do_run(cap_out, git_path, _get_opts(verbose=True)) + assert ret == 0 + # Does not contain foo.py since it was excluded + expected = b'hookid: bash_hook\n\nbar.py\nHello World\n\n' + assert printed.endswith(expected) + + def test_show_diff_on_failure( capfd, cap_out, tempdir_factory, mock_out_store_directory, ): From 773a817f7fa300c5561e7d27ff6a67b11c261fc5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 11 Sep 2017 09:07:45 -0700 Subject: [PATCH 077/544] v1.1.0 --- CHANGELOG.md | 18 +++++++++++++++++- setup.py | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 229e2d11b..fb362b8d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,23 @@ +1.1.0 +===== + +### Features +- pre-commit configuration gains a `fail_fast` option. + - You must be using the v2 configuration format introduced in 1.0.0. + - `fail_fast` defaults to `false`. + - #240 issue by @Lucas-C. + - #616 PR by @asottile. +- pre-commit configuration gains a global `exclude` option. + - This option takes a python regular expression and can be used to exclude + files from _all_ hooks. + - You must be using the v2 configuration format introduced in 1.0.0. + - #281 issue by @asieira. + - #617 PR by @asottile. + 1.0.1 ===== -## Fixes +### Fixes - Fix a regression in the return code of `pre-commit autoupdate` - `pre-commit migrate-config` and `pre-commit autoupdate` return 0 when successful. diff --git a/setup.py b/setup.py index 68c6a4c48..95d2ff002 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.0.1', + version='1.1.0', author='Anthony Sottile', author_email='asottile@umich.edu', From b907c02f0561239d35370aa9bb117c471aa5f499 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 14 Sep 2017 10:09:25 -0700 Subject: [PATCH 078/544] Also check the ssl module for virtualenv health. --- pre_commit/languages/python.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 7800e17a6..cc4f93a26 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -86,7 +86,8 @@ def get_default_version(): def healthy(repo_cmd_runner, language_version): with in_env(repo_cmd_runner, language_version): retcode, _, _ = cmd_output( - 'python', '-c', 'import datetime, io, os, weakref', retcode=None, + 'python', '-c', 'import datetime, io, os, ssl, weakref', + retcode=None, ) return retcode == 0 From bcf6321bd4094bce21276bccb8663c87073ad913 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 17 Sep 2017 15:22:48 -0700 Subject: [PATCH 079/544] Do not crash in staged_files_only if patch_dir does not exist --- pre_commit/staged_files_only.py | 2 ++ tests/staged_files_only_test.py | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index cfd638155..1d0c36488 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -8,6 +8,7 @@ from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output +from pre_commit.util import mkdirp logger = logging.getLogger('pre_commit') @@ -43,6 +44,7 @@ def staged_files_only(patch_dir): 'Stashing unstaged files to {}.'.format(patch_filename), ) # Save the current unstaged changes as a patch + mkdirp(patch_dir) with io.open(patch_filename, 'wb') as patch_file: patch_file.write(diff_stdout_binary) diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index aec55f5de..36b19855c 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -75,6 +75,15 @@ def test_foo_something_unstaged(foo_staged, patch_dir): _test_foo_state(foo_staged, 'herp\nderp\n', 'AM') +def test_does_not_crash_patch_dir_does_not_exist(foo_staged, patch_dir): + with io.open(foo_staged.foo_filename, 'w') as foo_file: + foo_file.write('hello\nworld\n') + + shutil.rmtree(patch_dir) + with staged_files_only(patch_dir): + pass + + def test_something_unstaged_ext_diff_tool(foo_staged, patch_dir, tmpdir): diff_tool = tmpdir.join('diff-tool.sh') diff_tool.write('#!/usr/bin/env bash\necho "$@"\n') From f4595dce8cddd4192e0d4b9e29e2701a9d4169d7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 17 Sep 2017 17:06:58 -0700 Subject: [PATCH 080/544] v1.1.1 --- CHANGELOG.md | 12 ++++++++++++ setup.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb362b8d4..421f355eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +1.1.1 +===== + +### Features +- pre-commit also checks the `ssl` module for virtualenv health + - Suggestion by @merwok. + - #619 PR by @asottile. +### Fixes +- pre-commit no longer crashes with unstaged files when run for the first time + - #620 #621 issue by @Lucas-C. + - #622 PR by @asottile. + 1.1.0 ===== diff --git a/setup.py b/setup.py index 95d2ff002..a2cd138b5 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.1.0', + version='1.1.1', author='Anthony Sottile', author_email='asottile@umich.edu', From d2097ade8b3ea423181a860c7ee9b496219d898d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 20 Sep 2017 05:29:13 -0700 Subject: [PATCH 081/544] Include all resources --- setup.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/setup.py b/setup.py index a2cd138b5..55116ca9a 100644 --- a/setup.py +++ b/setup.py @@ -29,11 +29,7 @@ packages=find_packages(exclude=('tests*', 'testing*')), package_data={ 'pre_commit': [ - 'resources/hook-tmpl', - 'resources/pre-push-tmpl', - 'resources/rbenv.tar.gz', - 'resources/ruby-build.tar.gz', - 'resources/ruby-download.tar.gz', + 'resources/*', 'resources/empty_template/*', 'resources/empty_template/.npmignore', ], From 6b81fe9d58731a410564cb58f0fd4d220b7bd288 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 20 Sep 2017 05:45:39 -0700 Subject: [PATCH 082/544] v1.1.2 --- CHANGELOG.md | 9 +++++++++ setup.py | 5 +++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 421f355eb..4e6fe248d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +1.1.2 +===== + +### Fixes +- pre-commit can successfully install commit-msg hooks + - Due to an oversight, the commit-msg-tmpl was missing from the packaging + - #623 issue by @sobolevn. + - #624 PR by @asottile. + 1.1.1 ===== diff --git a/setup.py b/setup.py index 55116ca9a..76a8bd798 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.1.1', + version='1.1.2', author='Anthony Sottile', author_email='asottile@umich.edu', @@ -29,7 +29,8 @@ packages=find_packages(exclude=('tests*', 'testing*')), package_data={ 'pre_commit': [ - 'resources/*', + 'resources/*-tmpl', + 'resources/*.tar.gz', 'resources/empty_template/*', 'resources/empty_template/.npmignore', ], From 873dd173ce1f40dd368f8ab243d2b65c7dfdd664 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 21 Sep 2017 15:16:48 -0700 Subject: [PATCH 083/544] Use pipes.quote for executable path --- pre_commit/commands/install_uninstall.py | 3 ++- pre_commit/resources/hook-tmpl | 2 +- tests/commands/install_uninstall_test.py | 5 +++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 6d9d14d82..01aad52d6 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -3,6 +3,7 @@ import io import os.path +import pipes import sys from pre_commit import output @@ -68,7 +69,7 @@ def install( skip_on_missing_conf = 'true' if skip_on_missing_conf else 'false' contents = io.open(resource_filename('hook-tmpl')).read().format( - sys_executable=sys.executable, + sys_executable=pipes.quote(sys.executable), hook_type=hook_type, hook_specific=hook_specific_contents, config_file=runner.config_file, diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 3a10e90c0..149bb768d 100644 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -9,7 +9,7 @@ popd > /dev/null retv=0 args="" -ENV_PYTHON='{sys_executable}' +ENV_PYTHON={sys_executable} SKIP_ON_MISSING_CONF={skip_on_missing_conf} which pre-commit >& /dev/null diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 80e249bea..2ba5ce36b 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -4,6 +4,7 @@ import io import os.path +import pipes import re import shutil import subprocess @@ -54,7 +55,7 @@ def test_install_pre_commit(tempdir_factory): pre_commit_contents = io.open(runner.pre_commit_path).read() pre_commit_script = resource_filename('hook-tmpl') expected_contents = io.open(pre_commit_script).read().format( - sys_executable=sys.executable, + sys_executable=pipes.quote(sys.executable), hook_type='pre-commit', hook_specific='', config_file=runner.config_file, @@ -70,7 +71,7 @@ def test_install_pre_commit(tempdir_factory): pre_push_tmpl = resource_filename('pre-push-tmpl') pre_push_template_contents = io.open(pre_push_tmpl).read() expected_contents = io.open(pre_commit_script).read().format( - sys_executable=sys.executable, + sys_executable=pipes.quote(sys.executable), hook_type='pre-push', hook_specific=pre_push_template_contents, config_file=runner.config_file, From 916ca72bb1e5db6b0c057f51bf2eeffbda858b8e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 21 Sep 2017 16:09:45 -0700 Subject: [PATCH 084/544] Use some bash best practices and simplify hook template --- pre_commit/resources/hook-tmpl | 54 ++++++++++++---------------------- 1 file changed, 18 insertions(+), 36 deletions(-) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 149bb768d..ded311cfa 100644 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -2,9 +2,9 @@ # This is a randomish md5 to identify this script # 138fd403232d2ddd5efb44317e38bf03 -pushd `dirname $0` > /dev/null -HERE=`pwd` -popd > /dev/null +pushd "$(dirname "$0")" >& /dev/null +HERE="$(pwd)" +popd >& /dev/null retv=0 args="" @@ -12,35 +12,28 @@ args="" ENV_PYTHON={sys_executable} SKIP_ON_MISSING_CONF={skip_on_missing_conf} -which pre-commit >& /dev/null -WHICH_RETV=$? -"$ENV_PYTHON" -c 'import pre_commit.main' >& /dev/null -ENV_PYTHON_RETV=$? -python -c 'import pre_commit.main' >& /dev/null -PYTHON_RETV=$? - - -if (( - (WHICH_RETV != 0) && - (ENV_PYTHON_RETV != 0) && - (PYTHON_RETV != 0) -)); then +if which pre-commit >& /dev/null; then + exe="pre-commit" + run_args="" +elif "$ENV_PYTHON" -c 'import pre_commit.main' >& /dev/null; then + exe="$ENV_PYTHON" + run_args="-m pre_commit.main" +elif python -c 'import pre_commit.main' >& /dev/null; then + exe="python" + run_args="-m pre_commit.main" +else echo '`pre-commit` not found. Did you forget to activate your virtualenv?' exit 1 fi - # Run the legacy pre-commit if it exists -if [ -x "$HERE"/{hook_type}.legacy ]; then - "$HERE"/{hook_type}.legacy - if [ $? -ne 0 ]; then - retv=1 - fi +if [ -x "$HERE"/{hook_type}.legacy ] && ! "$HERE"/{hook_type}.legacy; then + retv=1 fi CONF_FILE="$(git rev-parse --show-toplevel)/{config_file}" -if [ ! -f $CONF_FILE ]; then - if [ $SKIP_ON_MISSING_CONF = true ] || [ ! -z $PRE_COMMIT_ALLOW_NO_CONFIG ]; then +if [ ! -f "$CONF_FILE" ]; then + if [ "$SKIP_ON_MISSING_CONF" = true -o ! -z "$PRE_COMMIT_ALLOW_NO_CONFIG" ]; then echo '`{config_file}` config file not found. Skipping `pre-commit`.' exit $retv else @@ -55,18 +48,7 @@ fi {hook_specific} # Run pre-commit -if ((WHICH_RETV == 0)); then - pre-commit run $args --config {config_file} - PRE_COMMIT_RETV=$? -elif ((ENV_PYTHON_RETV == 0)); then - "$ENV_PYTHON" -m pre_commit.main run $args - PRE_COMMIT_RETV=$? -else - python -m pre_commit.main run $args - PRE_COMMIT_RETV=$? -fi - -if ((PRE_COMMIT_RETV != 0)); then +if ! "$exe" $run_args run $args --config {config_file}; then retv=1 fi From 989bcfe9ca61332e35be46ccd39ca73d186a0e22 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 22 Sep 2017 10:25:02 -0700 Subject: [PATCH 085/544] Use file:// protocol for cloning under test --- pre_commit/languages/golang.py | 5 ++++- testing/fixtures.py | 2 +- tests/languages/golang_test.py | 1 + tests/repository_test.py | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 87687234c..cad7dfc6d 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -37,8 +37,11 @@ def in_env(repo_cmd_runner): def guess_go_dir(remote_url): if remote_url.endswith('.git'): remote_url = remote_url[:-1 * len('.git')] + looks_like_url = ( + not remote_url.startswith('file://') and + ('//' in remote_url or '@' in remote_url) + ) remote_url = remote_url.replace(':', '/') - looks_like_url = '//' in remote_url or '@' in remote_url if looks_like_url: _, _, remote_url = remote_url.rpartition('//') _, _, remote_url = remote_url.rpartition('@') diff --git a/testing/fixtures.py b/testing/fixtures.py index 1c61b2b1b..befc3f53a 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -83,7 +83,7 @@ def config_with_local_hooks(): def make_config_from_repo(repo_path, sha=None, hooks=None, check=True): manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) config = OrderedDict(( - ('repo', repo_path), + ('repo', 'file://{}'.format(repo_path)), ('sha', sha or get_head_sha(repo_path)), ( 'hooks', diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py index e0c9ab429..483f41ead 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -10,6 +10,7 @@ ('url', 'expected'), ( ('/im/a/path/on/disk', 'unknown_src_dir'), + ('file:///im/a/path/on/disk', 'unknown_src_dir'), ('git@github.com:golang/lint', 'github.com/golang/lint'), ('git://github.com/golang/lint', 'github.com/golang/lint'), ('http://github.com/golang/lint', 'github.com/golang/lint'), diff --git a/tests/repository_test.py b/tests/repository_test.py index 6842800ea..8ff9db4cf 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -714,7 +714,7 @@ def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): with pytest.raises(SystemExit): repo.require_installed() assert fake_log_handler.handle.call_args[0][0].msg == ( - '`i-dont-exist` is not present in repository {}. ' + '`i-dont-exist` is not present in repository file://{}. ' 'Typo? Perhaps it is introduced in a newer version? ' 'Often `pre-commit autoupdate` fixes this.'.format(path) ) From e9509306d85150c95007f16d32cd9e1038ab02ca Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 22 Sep 2017 17:06:48 -0700 Subject: [PATCH 086/544] Implement pygrep language as a replacement for pcre --- pre_commit/languages/all.py | 2 + pre_commit/languages/pygrep.py | 59 ++++++ pre_commit/repository.py | 4 +- testing/fixtures.py | 2 +- .../pcre_hooks_repo/.pre-commit-hooks.yaml | 16 -- tests/languages/pygrep_test.py | 40 ++++ tests/repository_test.py | 171 ++++++++---------- tests/runner_test.py | 4 +- 8 files changed, 186 insertions(+), 112 deletions(-) create mode 100644 pre_commit/languages/pygrep.py delete mode 100644 testing/resources/pcre_hooks_repo/.pre-commit-hooks.yaml create mode 100644 tests/languages/pygrep_test.py diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 67b7ddea9..514ba6110 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -5,6 +5,7 @@ from pre_commit.languages import golang from pre_commit.languages import node from pre_commit.languages import pcre +from pre_commit.languages import pygrep from pre_commit.languages import python from pre_commit.languages import ruby from pre_commit.languages import script @@ -54,6 +55,7 @@ 'golang': golang, 'node': node, 'pcre': pcre, + 'pygrep': pygrep, 'python': python, 'ruby': ruby, 'script': script, diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py new file mode 100644 index 000000000..4914fd66d --- /dev/null +++ b/pre_commit/languages/pygrep.py @@ -0,0 +1,59 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import argparse +import re +import sys + +from pre_commit import output +from pre_commit.languages import helpers +from pre_commit.xargs import xargs + + +ENVIRONMENT_DIR = None +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy +install_environment = helpers.no_install + + +def _process_filename_by_line(pattern, filename): + retv = 0 + with open(filename, 'rb') as f: + for line_no, line in enumerate(f, start=1): + if pattern.search(line): + retv = 1 + output.write('{}:{}:'.format(filename, line_no)) + output.write_line(line.rstrip(b'\r\n')) + return retv + + +def run_hook(repo_cmd_runner, hook, file_args): + exe = (sys.executable, '-m', __name__) + exe += tuple(hook['args']) + (hook['entry'],) + return xargs(exe, file_args) + + +def main(argv=None): + parser = argparse.ArgumentParser( + description=( + 'grep-like finder using python regexes. Unlike grep, this tool ' + 'returns nonzero when it finds a match and zero otherwise. The ' + 'idea here being that matches are "problems".' + ), + ) + parser.add_argument('-i', '--ignore-case', action='store_true') + parser.add_argument('pattern', help='python regex pattern.') + parser.add_argument('filenames', nargs='*') + args = parser.parse_args(argv) + + flags = re.IGNORECASE if args.ignore_case else 0 + pattern = re.compile(args.pattern.encode(), flags) + + retv = 0 + for filename in args.filenames: + retv |= _process_filename_by_line(pattern, filename) + return retv + + +if __name__ == '__main__': + exit(main()) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 675c47161..6955a73ed 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -202,8 +202,8 @@ class LocalRepository(Repository): def _cmd_runner_from_deps(self, language_name, deps): """local repositories have a cmd runner per hook""" language = languages[language_name] - # pcre / script / system / docker_image do not have environments so - # they work out of the current directory + # pcre / pygrep / script / system / docker_image do not have + # environments so they work out of the current directory if language.ENVIRONMENT_DIR is None: return PrefixedCommandRunner(git.get_root()) else: diff --git a/testing/fixtures.py b/testing/fixtures.py index befc3f53a..388b344b6 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -73,7 +73,7 @@ def config_with_local_hooks(): ('id', 'do_not_commit'), ('name', 'Block if "DO NOT COMMIT" is found'), ('entry', 'DO NOT COMMIT'), - ('language', 'pcre'), + ('language', 'pygrep'), ('files', '^(.*)$'), ))], ), diff --git a/testing/resources/pcre_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/pcre_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 709d8df38..000000000 --- a/testing/resources/pcre_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,16 +0,0 @@ -- id: regex-with-quotes - name: Regex with quotes - entry: "foo'bar" - language: pcre - files: '' -- id: other-regex - name: Other regex - entry: ^\[INFO\] - language: pcre - files: '' -- id: regex-with-grep-args - name: Regex with grep extra arguments - entry: foo.+bar - language: pcre - files: '' - args: [-i] diff --git a/tests/languages/pygrep_test.py b/tests/languages/pygrep_test.py new file mode 100644 index 000000000..048a59088 --- /dev/null +++ b/tests/languages/pygrep_test.py @@ -0,0 +1,40 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import pytest + +from pre_commit.languages import pygrep + + +@pytest.fixture +def some_files(tmpdir): + tmpdir.join('f1').write_binary(b'foo\nbar\n') + tmpdir.join('f2').write_binary(b'[INFO] hi\n') + tmpdir.join('f3').write_binary(b"with'quotes\n") + with tmpdir.as_cwd(): + yield + + +@pytest.mark.usefixtures('some_files') +@pytest.mark.parametrize( + ('pattern', 'expected_retcode', 'expected_out'), + ( + ('baz', 0, ''), + ('foo', 1, 'f1:1:foo\n'), + ('bar', 1, 'f1:2:bar\n'), + (r'(?i)\[info\]', 1, 'f2:1:[INFO] hi\n'), + ("h'q", 1, "f3:1:with'quotes\n"), + ), +) +def test_main(some_files, cap_out, pattern, expected_retcode, expected_out): + ret = pygrep.main((pattern, 'f1', 'f2', 'f3')) + out = cap_out.get() + assert ret == expected_retcode + assert out == expected_out + + +def test_ignore_case(some_files, cap_out): + ret = pygrep.main(('--ignore-case', 'info', 'f1', 'f2', 'f3')) + out = cap_out.get() + assert ret == 1 + assert out == 'f2:1:[INFO] hi\n' diff --git a/tests/repository_test.py b/tests/repository_test.py index 8ff9db4cf..37a609baf 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1,6 +1,7 @@ from __future__ import absolute_import from __future__ import unicode_literals +import collections import io import os.path import re @@ -36,6 +37,10 @@ from testing.util import xfailif_windows_no_ruby +def _norm_out(b): + return b.replace(b'\r\n', b'\n') + + def _test_hook_repo( tempdir_factory, store, @@ -54,7 +59,7 @@ def _test_hook_repo( ] ret = repo.run_hook(hook_dict, args) assert ret[0] == expected_return_code - assert ret[1].replace(b'\r\n', b'\n') == expected + assert _norm_out(ret[1]) == expected @pytest.mark.integration @@ -114,7 +119,7 @@ def run_on_version(version, expected_output): ] ret = repo.run_hook(hook_dict, []) assert ret[0] == 0 - assert ret[1].replace(b'\r\n', b'\n') == expected_output + assert _norm_out(ret[1]) == expected_output run_on_version('python3.4', b'3.4\n[]\nHello World\n') run_on_version('python3.5', b'3.5\n[]\nHello World\n') @@ -277,25 +282,6 @@ def test_missing_executable(tempdir_factory, store): ) -@pytest.mark.integration -def test_missing_pcre_support(tempdir_factory, store): - orig_find_executable = parse_shebang.find_executable - - def no_grep(exe, **kwargs): - if exe == pcre.GREP: - return None - else: - return orig_find_executable(exe, **kwargs) - - with mock.patch.object(parse_shebang, 'find_executable', no_grep): - _test_hook_repo( - tempdir_factory, store, 'pcre_hooks_repo', - 'regex-with-quotes', ['/dev/null'], - 'Executable `{}` not found'.format(pcre.GREP).encode('UTF-8'), - expected_return_code=1, - ) - - @pytest.mark.integration def test_run_a_script_hook(tempdir_factory, store): _test_hook_repo( @@ -330,85 +316,88 @@ def test_run_hook_with_curly_braced_arguments(tempdir_factory, store): ) -@xfailif_no_pcre_support -@pytest.mark.integration -def test_pcre_hook_no_match(tempdir_factory, store): - path = git_dir(tempdir_factory) - with cwd(path): - with io.open('herp', 'w') as herp: - herp.write('foo') - - with io.open('derp', 'w') as derp: - derp.write('bar') - - _test_hook_repo( - tempdir_factory, store, 'pcre_hooks_repo', - 'regex-with-quotes', ['herp', 'derp'], b'', - ) - - _test_hook_repo( - tempdir_factory, store, 'pcre_hooks_repo', - 'other-regex', ['herp', 'derp'], b'', - ) - +def _make_grep_repo(language, entry, store, args=()): + config = collections.OrderedDict(( + ('repo', 'local'), + ( + 'hooks', [ + collections.OrderedDict(( + ('id', 'grep-hook'), + ('name', 'grep-hook'), + ('language', language), + ('entry', entry), + ('args', args), + ('types', ['text']), + )), + ], + ), + )) + repo = Repository.create(config, store) + (_, hook), = repo.hooks + return repo, hook -@xfailif_no_pcre_support -@pytest.mark.integration -def test_pcre_hook_matching(tempdir_factory, store): - path = git_dir(tempdir_factory) - with cwd(path): - with io.open('herp', 'w') as herp: - herp.write("\nherpfoo'bard\n") - with io.open('derp', 'w') as derp: - derp.write('[INFO] information yo\n') +@pytest.fixture +def greppable_files(tmpdir): + with tmpdir.as_cwd(): + cmd_output('git', 'init', '.') + tmpdir.join('f1').write_binary(b"hello'hi\nworld\n") + tmpdir.join('f2').write_binary(b'foo\nbar\nbaz\n') + tmpdir.join('f3').write_binary(b'[WARN] hi\n') + yield tmpdir - _test_hook_repo( - tempdir_factory, store, 'pcre_hooks_repo', - 'regex-with-quotes', ['herp', 'derp'], b"herp:2:herpfoo'bard\n", - expected_return_code=1, - ) - _test_hook_repo( - tempdir_factory, store, 'pcre_hooks_repo', - 'other-regex', ['herp', 'derp'], b'derp:1:[INFO] information yo\n', - expected_return_code=1, - ) +class TestPygrep(object): + language = 'pygrep' + def test_grep_hook_matching(self, greppable_files, store): + repo, hook = _make_grep_repo(self.language, 'ello', store) + ret, out, _ = repo.run_hook(hook, ('f1', 'f2', 'f3')) + assert ret == 1 + assert _norm_out(out) == b"f1:1:hello'hi\n" -@xfailif_no_pcre_support -@pytest.mark.integration -def test_pcre_hook_case_insensitive_option(tempdir_factory, store): - path = git_dir(tempdir_factory) - with cwd(path): - with io.open('herp', 'w') as herp: - herp.write('FoOoOoObar\n') + def test_grep_hook_case_insensitive(self, greppable_files, store): + repo, hook = _make_grep_repo(self.language, 'ELLO', store, args=['-i']) + ret, out, _ = repo.run_hook(hook, ('f1', 'f2', 'f3')) + assert ret == 1 + assert _norm_out(out) == b"f1:1:hello'hi\n" - _test_hook_repo( - tempdir_factory, store, 'pcre_hooks_repo', - 'regex-with-grep-args', ['herp'], b'herp:1:FoOoOoObar\n', - expected_return_code=1, - ) + @pytest.mark.parametrize('regex', ('nope', "foo'bar", r'^\[INFO\]')) + def test_grep_hook_not_matching(self, regex, greppable_files, store): + repo, hook = _make_grep_repo(self.language, regex, store) + ret, out, _ = repo.run_hook(hook, ('f1', 'f2', 'f3')) + assert (ret, out) == (0, b'') @xfailif_no_pcre_support -@pytest.mark.integration -def test_pcre_many_files(tempdir_factory, store): - # This is intended to simulate lots of passing files and one failing file - # to make sure it still fails. This is not the case when naively using - # a system hook with `grep -H -n '...'` and expected_return_code=1. - path = git_dir(tempdir_factory) - with cwd(path): - with io.open('herp', 'w') as herp: - herp.write('[INFO] info\n') - - _test_hook_repo( - tempdir_factory, store, 'pcre_hooks_repo', - 'other-regex', - ['/dev/null'] * 15000 + ['herp'], - b'herp:1:[INFO] info\n', - expected_return_code=1, - ) +class TestPCRE(TestPygrep): + """organized as a class for xfailing pcre""" + language = 'pcre' + + def test_pcre_hook_many_files(self, greppable_files, store): + # This is intended to simulate lots of passing files and one failing + # file to make sure it still fails. This is not the case when naively + # using a system hook with `grep -H -n '...'` + repo, hook = _make_grep_repo('pcre', 'ello', store) + ret, out, _ = repo.run_hook(hook, (os.devnull,) * 15000 + ('f1',)) + assert ret == 1 + assert _norm_out(out) == b"f1:1:hello'hi\n" + + def test_missing_pcre_support(self, greppable_files, store): + orig_find_executable = parse_shebang.find_executable + + def no_grep(exe, **kwargs): + if exe == pcre.GREP: + return None + else: + return orig_find_executable(exe, **kwargs) + + with mock.patch.object(parse_shebang, 'find_executable', no_grep): + repo, hook = _make_grep_repo('pcre', 'ello', store) + ret, out, _ = repo.run_hook(hook, ('f1', 'f2', 'f3')) + assert ret == 1 + expected = 'Executable `{}` not found'.format(pcre.GREP).encode() + assert out == expected def _norm_pwd(path): @@ -703,7 +692,7 @@ def test_local_python_repo(store): (_, hook), = repo.hooks ret = repo.run_hook(hook, ('filename',)) assert ret[0] == 0 - assert ret[1].replace(b'\r\n', b'\n') == b"['filename']\nHello World\n" + assert _norm_out(ret[1]) == b"['filename']\nHello World\n" def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): diff --git a/tests/runner_test.py b/tests/runner_test.py index cfca44f36..b5c0ce756 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -70,7 +70,7 @@ def test_local_hooks(tempdir_factory, mock_out_store_directory): ('id', 'do_not_commit'), ('name', 'Block if "DO NOT COMMIT" is found'), ('entry', 'DO NOT COMMIT'), - ('language', 'pcre'), + ('language', 'pygrep'), ('files', '^(.*)$'), )), ), @@ -105,7 +105,7 @@ def test_local_hooks_alt_config(tempdir_factory, mock_out_store_directory): ('id', 'do_not_commit'), ('name', 'Block if "DO NOT COMMIT" is found'), ('entry', 'DO NOT COMMIT'), - ('language', 'pcre'), + ('language', 'pygrep'), ('files', '^(.*)$'), )), ), From 18c9e061d89e5a1386b6beda13399e2e030a7a4d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 30 Sep 2017 15:15:52 -0700 Subject: [PATCH 087/544] Small cleanups --- pre_commit/clientlib.py | 5 ++--- pre_commit/color.py | 5 +---- pre_commit/color_windows.py | 5 ++--- pre_commit/commands/run.py | 6 ++---- pre_commit/constants.py | 1 + pre_commit/envcontext.py | 6 ++---- pre_commit/error_handler.py | 9 +++++---- pre_commit/errors.py | 6 ------ pre_commit/file_lock.py | 3 +++ pre_commit/five.py | 1 + pre_commit/git.py | 5 ++--- pre_commit/logging_handler.py | 4 ++-- pre_commit/make_archives.py | 14 +++++++------- pre_commit/util.py | 8 +++----- tests/error_handler_test.py | 7 +++---- tests/git_test.py | 2 +- tests/make_archives_test.py | 11 +++-------- 17 files changed, 40 insertions(+), 58 deletions(-) delete mode 100644 pre_commit/errors.py diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index c04cf333e..11750b741 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -10,7 +10,7 @@ import pre_commit.constants as C from pre_commit import schema -from pre_commit.errors import FatalError +from pre_commit.error_handler import FatalError from pre_commit.languages.all import all_languages @@ -51,8 +51,7 @@ def _make_argparser(filenames_help): '', ), schema.Optional( - 'exclude', - schema.check_and(schema.check_string, schema.check_regex), + 'exclude', schema.check_and(schema.check_string, schema.check_regex), '^$', ), schema.Optional('types', schema.check_array(check_type_tag), ['file']), diff --git a/pre_commit/color.py b/pre_commit/color.py index 25fbb2568..44917ca04 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -47,7 +47,4 @@ def use_color(setting): if setting not in COLOR_CHOICES: raise InvalidColorSetting(setting) - return ( - setting == 'always' or - (setting == 'auto' and sys.stdout.isatty()) - ) + return setting == 'always' or (setting == 'auto' and sys.stdout.isatty()) diff --git a/pre_commit/color_windows.py b/pre_commit/color_windows.py index dae41afe8..4e193f967 100644 --- a/pre_commit/color_windows.py +++ b/pre_commit/color_windows.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from __future__ import unicode_literals from ctypes import POINTER @@ -19,8 +20,7 @@ def bool_errcheck(result, func, args): GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)( - ("GetStdHandle", windll.kernel32), - ((1, "nStdHandle"), ), + ("GetStdHandle", windll.kernel32), ((1, "nStdHandle"),), ) GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))( @@ -42,7 +42,6 @@ def enable_virtual_terminal_processing(): More info on the escape sequences supported: https://msdn.microsoft.com/en-us/library/windows/desktop/mt638032(v=vs.85).aspx - """ stdout = GetStdHandle(STD_OUTPUT_HANDLE) flags = GetConsoleMode(stdout) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 6f6954874..74bff891e 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -32,8 +32,7 @@ def _get_skips(environ): def _hook_msg_start(hook, verbose): return '{}{}'.format( - '[{}] '.format(hook['id']) if verbose else '', - hook['name'], + '[{}] '.format(hook['id']) if verbose else '', hook['name'], ) @@ -99,8 +98,7 @@ def _run_single_hook(filenames, hook, repo, args, skips, cols): 'git', 'diff', '--no-ext-diff', retcode=None, encoding=None, ) retcode, stdout, stderr = repo.run_hook( - hook, - tuple(filenames) if hook['pass_filenames'] else (), + hook, tuple(filenames) if hook['pass_filenames'] else (), ) diff_after = cmd_output( 'git', 'diff', '--no-ext-diff', retcode=None, encoding=None, diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 8af491846..2fa435522 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from __future__ import unicode_literals import pkg_resources diff --git a/pre_commit/envcontext.py b/pre_commit/envcontext.py index 8066da3bd..82538df22 100644 --- a/pre_commit/envcontext.py +++ b/pre_commit/envcontext.py @@ -10,14 +10,12 @@ Var = collections.namedtuple('Var', ('name', 'default')) -setattr(Var.__new__, '__defaults__', ('',)) +Var.__new__.__defaults__ = ('',) def format_env(parts, env): return ''.join( - env.get(part.name, part.default) - if isinstance(part, Var) - else part + env.get(part.name, part.default) if isinstance(part, Var) else part for part in parts ) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 76662e972..720678032 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -10,10 +10,13 @@ from pre_commit import five from pre_commit import output -from pre_commit.errors import FatalError from pre_commit.store import Store +class FatalError(RuntimeError): + pass + + def _to_bytes(exc): try: return bytes(exc) @@ -46,7 +49,5 @@ def error_handler(): _log_and_exit('An error has occurred', e, traceback.format_exc()) except Exception as e: _log_and_exit( - 'An unexpected error has occurred', - e, - traceback.format_exc(), + 'An unexpected error has occurred', e, traceback.format_exc(), ) diff --git a/pre_commit/errors.py b/pre_commit/errors.py deleted file mode 100644 index 4dedbfc2d..000000000 --- a/pre_commit/errors.py +++ /dev/null @@ -1,6 +0,0 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - - -class FatalError(RuntimeError): - pass diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index f33584c3d..7c7e85143 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -1,3 +1,6 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + import contextlib import errno diff --git a/pre_commit/five.py b/pre_commit/five.py index de017267f..3b94a927a 100644 --- a/pre_commit/five.py +++ b/pre_commit/five.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from __future__ import unicode_literals import six diff --git a/pre_commit/git.py b/pre_commit/git.py index 1c3191e3a..96a5155be 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -4,7 +4,7 @@ import os.path import sys -from pre_commit.errors import FatalError +from pre_commit.error_handler import FatalError from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output @@ -114,7 +114,6 @@ def check_for_cygwin_mismatch(): 'These can be installed through the cygwin installer.\n' ' - python {}\n' ' - git {}\n'.format( - exe_type[is_cygwin_python], - exe_type[is_cygwin_git], + exe_type[is_cygwin_python], exe_type[is_cygwin_git], ), ) diff --git a/pre_commit/logging_handler.py b/pre_commit/logging_handler.py index 7241cd679..c043a8ac2 100644 --- a/pre_commit/logging_handler.py +++ b/pre_commit/logging_handler.py @@ -23,12 +23,12 @@ def __init__(self, use_color): def emit(self, record): output.write_line( - '{}{}'.format( + '{} {}'.format( color.format_color( '[{}]'.format(record.levelname), LOG_LEVEL_COLORS[record.levelname], self.use_color, - ) + ' ', + ), record.getMessage(), ), ) diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index c672fc181..90809c106 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -2,12 +2,14 @@ from __future__ import print_function from __future__ import unicode_literals +import argparse import os.path import tarfile from pre_commit import output from pre_commit.util import cmd_output from pre_commit.util import cwd +from pre_commit.util import resource_filename from pre_commit.util import rmtree from pre_commit.util import tmpdir @@ -27,11 +29,6 @@ ) -RESOURCES_DIR = os.path.abspath( - os.path.join(os.path.dirname(__file__), 'resources'), -) - - def make_archive(name, repo, ref, destdir): """Makes an archive of a repository in the given destdir. @@ -59,12 +56,15 @@ def make_archive(name, repo, ref, destdir): return output_path -def main(): +def main(argv=None): + parser = argparse.ArgumentParser() + parser.add_argument('--dest', default=resource_filename()) + args = parser.parse_args(argv) for archive_name, repo, ref in REPOS: output.write_line('Making {}.tar.gz for {}@{}'.format( archive_name, repo, ref, )) - make_archive(archive_name, repo, ref, RESOURCES_DIR) + make_archive(archive_name, repo, ref, args.dest) if __name__ == '__main__': diff --git a/pre_commit/util.py b/pre_commit/util.py index b0095843b..10d78d99f 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -93,18 +93,16 @@ def tmpdir(): rmtree(tempdir) -def resource_filename(filename): +def resource_filename(*segments): return pkg_resources.resource_filename( - 'pre_commit', - os.path.join('resources', filename), + 'pre_commit', os.path.join('resources', *segments), ) def make_executable(filename): original_mode = os.stat(filename).st_mode os.chmod( - filename, - original_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH, + filename, original_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH, ) diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index d6eaf500b..0e93298b0 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -11,7 +11,6 @@ import pytest from pre_commit import error_handler -from pre_commit.errors import FatalError from testing.util import cmd_output_mocked_pre_commit_home @@ -28,7 +27,7 @@ def test_error_handler_no_exception(mocked_log_and_exit): def test_error_handler_fatal_error(mocked_log_and_exit): - exc = FatalError('just a test') + exc = error_handler.FatalError('just a test') with error_handler.error_handler(): raise exc @@ -46,7 +45,7 @@ def test_error_handler_fatal_error(mocked_log_and_exit): r' File ".+tests.error_handler_test.py", line \d+, ' r'in test_error_handler_fatal_error\n' r' raise exc\n' - r'(pre_commit\.errors\.)?FatalError: just a test\n', + r'(pre_commit\.error_handler\.)?FatalError: just a test\n', mocked_log_and_exit.call_args[0][2], ) @@ -77,7 +76,7 @@ def test_error_handler_uncaught_error(mocked_log_and_exit): def test_log_and_exit(cap_out, mock_out_store_directory): with pytest.raises(SystemExit): error_handler._log_and_exit( - 'msg', FatalError('hai'), "I'm a stacktrace", + 'msg', error_handler.FatalError('hai'), "I'm a stacktrace", ) printed = cap_out.get() diff --git a/tests/git_test.py b/tests/git_test.py index 8417523fe..8f80dcade 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -7,7 +7,7 @@ import pytest from pre_commit import git -from pre_commit.errors import FatalError +from pre_commit.error_handler import FatalError from pre_commit.util import cmd_output from pre_commit.util import cwd from testing.fixtures import git_dir diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py index 5aa303f77..34233424e 100644 --- a/tests/make_archives_test.py +++ b/tests/make_archives_test.py @@ -4,7 +4,6 @@ import os.path import tarfile -import mock import pytest from pre_commit import make_archives @@ -53,12 +52,8 @@ def test_make_archive(tempdir_factory): @skipif_slowtests_false @pytest.mark.integration -def test_main(tempdir_factory): - path = tempdir_factory.get() - - # Don't actually want to make these in the current repo - with mock.patch.object(make_archives, 'RESOURCES_DIR', path): - make_archives.main() +def test_main(tmpdir): + make_archives.main(('--dest', tmpdir.strpath)) for archive, _, _ in make_archives.REPOS: - assert os.path.exists(os.path.join(path, archive + '.tar.gz')) + assert tmpdir.join('{}.tar.gz'.format(archive)).exists() From e70825ab317dafa81cd8fb2e414594687de0dcdf Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 3 Oct 2017 21:10:09 -0700 Subject: [PATCH 088/544] Add ctypes to healthy check --- pre_commit/languages/python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index cc4f93a26..328524090 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -86,7 +86,7 @@ def get_default_version(): def healthy(repo_cmd_runner, language_version): with in_env(repo_cmd_runner, language_version): retcode, _, _ = cmd_output( - 'python', '-c', 'import datetime, io, os, ssl, weakref', + 'python', '-c', 'import ctypes, datetime, io, os, ssl, weakref', retcode=None, ) return retcode == 0 From 883bd4204629bd5e6c7dac1aaa165a3600d63494 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 3 Oct 2017 21:16:16 -0700 Subject: [PATCH 089/544] v1.2.0 --- CHANGELOG.md | 22 ++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e6fe248d..0d3388bfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +1.2.0 +===== + +### Features +- Add `pygrep` language + - `pygrep` aims to be a more cross-platform alternative to `pcre` hooks. + - #630 PR by @asottile. + +### Fixes +- Use `pipes.quote` for executable path in hook template + - Fixes bash syntax error when git dir contains spaces + - #626 PR by @asottile. +- Clean up hook template + - Simplify code + - Fix `--config` not being respected in some situations + - #627 PR by @asottile. +- Use `file://` protocol for cloning under test + - Fix `file://` clone paths being treated as urls for golang +- Add `ctypes` as an import for virtualenv healthchecks + - Fixes python3.6.2 <=> python3.6.3 virtualenv invalidation + - e70825ab by @asottile. + 1.1.2 ===== diff --git a/setup.py b/setup.py index 76a8bd798..fbb2397c5 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.1.2', + version='1.2.0', author='Anthony Sottile', author_email='asottile@umich.edu', From e8641ee0a30ff04c71839d874d68ba51f85f21be Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 3 Oct 2017 21:21:24 -0700 Subject: [PATCH 090/544] Forgot a line in the changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d3388bfc..6a6160db1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - #627 PR by @asottile. - Use `file://` protocol for cloning under test - Fix `file://` clone paths being treated as urls for golang + - #629 PR by @asottile. - Add `ctypes` as an import for virtualenv healthchecks - Fixes python3.6.2 <=> python3.6.3 virtualenv invalidation - e70825ab by @asottile. From 2c88791a7fc08f961e041cefcd9eb286d0b47aa5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 7 Oct 2017 15:13:53 -0700 Subject: [PATCH 091/544] Add `pre-commit try-repo` `try-repo` is useful for: - Trying out a remote hook repository without needing to configure it. - Testing a hook repository while developing it. --- pre_commit/commands/try_repo.py | 44 +++++++++++++++ pre_commit/git.py | 5 ++ pre_commit/main.py | 91 +++++++++++++++++++------------ pre_commit/manifest.py | 3 +- pre_commit/repository.py | 2 +- pre_commit/runner.py | 7 ++- testing/fixtures.py | 4 +- testing/util.py | 35 ++++++++++-- tests/commands/autoupdate_test.py | 12 ++-- tests/commands/run_test.py | 62 ++++++--------------- tests/commands/try_repo_test.py | 71 ++++++++++++++++++++++++ tests/main_test.py | 8 ++- tests/make_archives_test.py | 4 +- tests/manifest_test.py | 10 ++-- tests/store_test.py | 6 +- 15 files changed, 254 insertions(+), 110 deletions(-) create mode 100644 pre_commit/commands/try_repo.py create mode 100644 tests/commands/try_repo_test.py diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py new file mode 100644 index 000000000..2e1933d81 --- /dev/null +++ b/pre_commit/commands/try_repo.py @@ -0,0 +1,44 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import collections +import os.path + +from aspy.yaml import ordered_dump + +import pre_commit.constants as C +from pre_commit import git +from pre_commit import output +from pre_commit.commands.run import run +from pre_commit.manifest import Manifest +from pre_commit.runner import Runner +from pre_commit.store import Store +from pre_commit.util import tmpdir + + +def try_repo(args): + ref = args.ref or git.head_sha(args.repo) + + with tmpdir() as tempdir: + if args.hook: + hooks = [{'id': args.hook}] + else: + manifest = Manifest(Store(tempdir).clone(args.repo, ref)) + hooks = [{'id': hook_id} for hook_id in sorted(manifest.hooks)] + + items = (('repo', args.repo), ('sha', ref), ('hooks', hooks)) + config = {'repos': [collections.OrderedDict(items)]} + config_s = ordered_dump(config, **C.YAML_DUMP_KWARGS) + + config_filename = os.path.join(tempdir, C.CONFIG_FILE) + with open(config_filename, 'w') as cfg: + cfg.write(config_s) + + output.write_line('=' * 79) + output.write_line('Using config:') + output.write_line('=' * 79) + output.write(config_s) + output.write_line('=' * 79) + + runner = Runner('.', config_filename, store_dir=tempdir) + return run(runner, args) diff --git a/pre_commit/git.py b/pre_commit/git.py index 96a5155be..c38b83ab0 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -97,6 +97,11 @@ def get_changed_files(new, old): )[1]) +def head_sha(remote): + _, out, _ = cmd_output('git', 'ls-remote', '--exit-code', remote, 'HEAD') + return out.split()[0] + + def check_for_cygwin_mismatch(): """See https://github.com/pre-commit/pre-commit/issues/354""" if sys.platform in ('cygwin', 'win32'): # pragma: no cover (windows) diff --git a/pre_commit/main.py b/pre_commit/main.py index 9167ee230..1405203c1 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -17,6 +17,7 @@ from pre_commit.commands.migrate_config import migrate_config from pre_commit.commands.run import run from pre_commit.commands.sample_config import sample_config +from pre_commit.commands.try_repo import try_repo from pre_commit.error_handler import error_handler from pre_commit.logging_handler import add_logging_handler from pre_commit.runner import Runner @@ -53,6 +54,41 @@ def _add_hook_type_option(parser): ) +def _add_run_options(parser): + parser.add_argument('hook', nargs='?', help='A single hook-id to run') + parser.add_argument('--verbose', '-v', action='store_true', default=False) + parser.add_argument( + '--origin', '-o', + help="The origin branch's commit_id when using `git push`.", + ) + parser.add_argument( + '--source', '-s', + help="The remote branch's commit_id when using `git push`.", + ) + parser.add_argument( + '--commit-msg-filename', + help='Filename to check when running during `commit-msg`', + ) + parser.add_argument( + '--hook-stage', choices=('commit', 'push', 'commit-msg'), + default='commit', + help='The stage during which the hook is fired e.g. commit or push.', + ) + parser.add_argument( + '--show-diff-on-failure', action='store_true', + help='When hooks fail, run `git diff` directly afterward.', + ) + mutex_group = parser.add_mutually_exclusive_group(required=False) + mutex_group.add_argument( + '--all-files', '-a', action='store_true', default=False, + help='Run on all the files in the repo.', + ) + mutex_group.add_argument( + '--files', nargs='*', default=[], + help='Specific filenames to run hooks on.', + ) + + def main(argv=None): argv = argv if argv is not None else sys.argv[1:] argv = [five.to_text(arg) for arg in argv] @@ -142,40 +178,7 @@ def main(argv=None): run_parser = subparsers.add_parser('run', help='Run hooks.') _add_color_option(run_parser) _add_config_option(run_parser) - run_parser.add_argument('hook', nargs='?', help='A single hook-id to run') - run_parser.add_argument( - '--verbose', '-v', action='store_true', default=False, - ) - run_parser.add_argument( - '--origin', '-o', - help="The origin branch's commit_id when using `git push`.", - ) - run_parser.add_argument( - '--source', '-s', - help="The remote branch's commit_id when using `git push`.", - ) - run_parser.add_argument( - '--commit-msg-filename', - help='Filename to check when running during `commit-msg`', - ) - run_parser.add_argument( - '--hook-stage', choices=('commit', 'push', 'commit-msg'), - default='commit', - help='The stage during which the hook is fired e.g. commit or push.', - ) - run_parser.add_argument( - '--show-diff-on-failure', action='store_true', - help='When hooks fail, run `git diff` directly afterward.', - ) - run_mutex_group = run_parser.add_mutually_exclusive_group(required=False) - run_mutex_group.add_argument( - '--all-files', '-a', action='store_true', default=False, - help='Run on all the files in the repo.', - ) - run_mutex_group.add_argument( - '--files', nargs='*', default=[], - help='Specific filenames to run hooks on.', - ) + _add_run_options(run_parser) sample_config_parser = subparsers.add_parser( 'sample-config', help='Produce a sample {} file'.format(C.CONFIG_FILE), @@ -183,6 +186,24 @@ def main(argv=None): _add_color_option(sample_config_parser) _add_config_option(sample_config_parser) + try_repo_parser = subparsers.add_parser( + 'try-repo', + help='Try the hooks in a repository, useful for developing new hooks.', + ) + _add_color_option(try_repo_parser) + _add_config_option(try_repo_parser) + try_repo_parser.add_argument( + 'repo', help='Repository to source hooks from.', + ) + try_repo_parser.add_argument( + '--ref', + help=( + 'Manually select a ref to run against, otherwise the `HEAD` ' + 'revision will be used.' + ), + ) + _add_run_options(try_repo_parser) + help = subparsers.add_parser( 'help', help='Show help for a specific command.', ) @@ -231,6 +252,8 @@ def main(argv=None): return run(runner, args) elif args.command == 'sample-config': return sample_config() + elif args.command == 'try-repo': + return try_repo(args) else: raise NotImplementedError( 'Command {} not implemented.'.format(args.command), diff --git a/pre_commit/manifest.py b/pre_commit/manifest.py index df2884428..99d839308 100644 --- a/pre_commit/manifest.py +++ b/pre_commit/manifest.py @@ -14,9 +14,8 @@ class Manifest(object): - def __init__(self, repo_path, repo_url): + def __init__(self, repo_path): self.repo_path = repo_path - self.repo_url = repo_url @cached_property def manifest_contents(self): diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 6955a73ed..4d7f0a5a0 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -146,7 +146,7 @@ def _cmd_runner_from_deps(self, language_name, deps): @cached_property def manifest(self): - return Manifest(self._repo_path, self.repo_config['repo']) + return Manifest(self._repo_path) @cached_property def hooks(self): diff --git a/pre_commit/runner.py b/pre_commit/runner.py index d853868a4..1983bab84 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -15,13 +15,14 @@ class Runner(object): repository under test. """ - def __init__(self, git_root, config_file): + def __init__(self, git_root, config_file, store_dir=None): self.git_root = git_root self.config_file = config_file + self._store_dir = store_dir @classmethod def create(cls, config_file): - """Creates a PreCommitRunner by doing the following: + """Creates a Runner by doing the following: - Finds the root of the current git repository - chdir to that directory """ @@ -63,4 +64,4 @@ def pre_push_path(self): @cached_property def store(self): - return Store() + return Store(self._store_dir) diff --git a/testing/fixtures.py b/testing/fixtures.py index 388b344b6..b1c7a89f0 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -10,6 +10,7 @@ from aspy.yaml import ordered_load import pre_commit.constants as C +from pre_commit import git from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest from pre_commit.schema import apply_defaults @@ -17,7 +18,6 @@ from pre_commit.util import cmd_output from pre_commit.util import copy_tree_to_path from pre_commit.util import cwd -from testing.util import get_head_sha from testing.util import get_resource_path @@ -84,7 +84,7 @@ def make_config_from_repo(repo_path, sha=None, hooks=None, check=True): manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) config = OrderedDict(( ('repo', 'file://{}'.format(repo_path)), - ('sha', sha or get_head_sha(repo_path)), + ('sha', sha or git.head_sha(repo_path)), ( 'hooks', hooks or [OrderedDict((('id', hook['id']),)) for hook in manifest], diff --git a/testing/util.py b/testing/util.py index 332b64185..8a5bfc4d3 100644 --- a/testing/util.py +++ b/testing/util.py @@ -8,7 +8,7 @@ from pre_commit.languages.docker import docker_is_running from pre_commit.languages.pcre import GREP from pre_commit.util import cmd_output -from pre_commit.util import cwd +from testing.auto_namedtuple import auto_namedtuple TESTING_DIR = os.path.abspath(os.path.dirname(__file__)) @@ -18,11 +18,6 @@ def get_resource_path(path): return os.path.join(TESTING_DIR, 'resources', path) -def get_head_sha(dir): - with cwd(dir): - return cmd_output('git', 'rev-parse', 'HEAD')[1].strip() - - def cmd_output_mocked_pre_commit_home(*args, **kwargs): # keyword-only argument tempdir_factory = kwargs.pop('tempdir_factory') @@ -72,3 +67,31 @@ def platform_supports_pcre(): not hasattr(os, 'symlink'), reason='Symlink is not supported on this platform', ) + + +def run_opts( + all_files=False, + files=(), + color=False, + verbose=False, + hook=None, + origin='', + source='', + hook_stage='commit', + show_diff_on_failure=False, + commit_msg_filename='', +): + # These are mutually exclusive + assert not (all_files and files) + return auto_namedtuple( + all_files=all_files, + files=files, + color=color, + verbose=verbose, + hook=hook, + origin=origin, + source=source, + hook_stage=hook_stage, + show_diff_on_failure=show_diff_on_failure, + commit_msg_filename=commit_msg_filename, + ) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 2877c5b3d..9ae70c641 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -7,6 +7,7 @@ import pytest import pre_commit.constants as C +from pre_commit import git from pre_commit.clientlib import load_config from pre_commit.commands.autoupdate import _update_repo from pre_commit.commands.autoupdate import autoupdate @@ -21,7 +22,6 @@ from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo from testing.fixtures import write_config -from testing.util import get_head_sha from testing.util import get_resource_path @@ -66,10 +66,10 @@ def test_autoupdate_old_revision_broken( cmd_output('git', 'mv', C.MANIFEST_FILE, 'nope.yaml') cmd_output('git', 'commit', '-m', 'simulate old repo') # Assume this is the revision the user's old repository was at - rev = get_head_sha(path) + rev = git.head_sha(path) cmd_output('git', 'mv', 'nope.yaml', C.MANIFEST_FILE) cmd_output('git', 'commit', '-m', 'move hooks file') - update_rev = get_head_sha(path) + update_rev = git.head_sha(path) config['sha'] = rev write_config('.', config) @@ -84,12 +84,12 @@ def test_autoupdate_old_revision_broken( @pytest.yield_fixture def out_of_date_repo(tempdir_factory): path = make_repo(tempdir_factory, 'python_hooks_repo') - original_sha = get_head_sha(path) + original_sha = git.head_sha(path) # Make a commit with cwd(path): cmd_output('git', 'commit', '--allow-empty', '-m', 'foo') - head_sha = get_head_sha(path) + head_sha = git.head_sha(path) yield auto_namedtuple( path=path, original_sha=original_sha, head_sha=head_sha, @@ -225,7 +225,7 @@ def test_autoupdate_tags_only( @pytest.yield_fixture def hook_disappearing_repo(tempdir_factory): path = make_repo(tempdir_factory, 'python_hooks_repo') - original_sha = get_head_sha(path) + original_sha = git.head_sha(path) with cwd(path): shutil.copy( diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 51e4eac91..d6812ae5a 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -20,12 +20,12 @@ from pre_commit.util import cmd_output from pre_commit.util import cwd from pre_commit.util import make_executable -from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import add_config_to_repo from testing.fixtures import make_consuming_repo from testing.fixtures import modify_config from testing.fixtures import read_config from testing.util import cmd_output_mocked_pre_commit_home +from testing.util import run_opts from testing.util import xfailif_no_symlink @@ -48,34 +48,6 @@ def stage_a_file(filename='foo.py'): cmd_output('git', 'add', filename) -def _get_opts( - all_files=False, - files=(), - color=False, - verbose=False, - hook=None, - origin='', - source='', - hook_stage='commit', - show_diff_on_failure=False, - commit_msg_filename='', -): - # These are mutually exclusive - assert not (all_files and files) - return auto_namedtuple( - all_files=all_files, - files=files, - color=color, - verbose=verbose, - hook=hook, - origin=origin, - source=source, - hook_stage=hook_stage, - show_diff_on_failure=show_diff_on_failure, - commit_msg_filename=commit_msg_filename, - ) - - def _do_run(cap_out, repo, args, environ={}, config_file=C.CONFIG_FILE): runner = Runner(repo, config_file) with cwd(runner.git_root): # replicates Runner.create behaviour @@ -90,7 +62,7 @@ def _test_run( ): if stage: stage_a_file() - args = _get_opts(**opts) + args = run_opts(**opts) ret, printed = _do_run(cap_out, repo, args, config_file=config_file) assert ret == expected_ret, (ret, expected_ret, printed) @@ -161,7 +133,7 @@ def test_types_hook_repository( with cwd(git_path): stage_a_file('bar.py') stage_a_file('bar.notpy') - ret, printed = _do_run(cap_out, git_path, _get_opts()) + ret, printed = _do_run(cap_out, git_path, run_opts()) assert ret == 1 assert b'bar.py' in printed assert b'bar.notpy' not in printed @@ -177,7 +149,7 @@ def test_exclude_types_hook_repository( make_executable('exe') cmd_output('git', 'add', 'exe') stage_a_file('bar.py') - ret, printed = _do_run(cap_out, git_path, _get_opts()) + ret, printed = _do_run(cap_out, git_path, run_opts()) assert ret == 1 assert b'bar.py' in printed assert b'exe' not in printed @@ -191,7 +163,7 @@ def test_global_exclude(cap_out, tempdir_factory, mock_out_store_directory): open('foo.py', 'a').close() open('bar.py', 'a').close() cmd_output('git', 'add', '.') - ret, printed = _do_run(cap_out, git_path, _get_opts(verbose=True)) + ret, printed = _do_run(cap_out, git_path, run_opts(verbose=True)) assert ret == 0 # Does not contain foo.py since it was excluded expected = b'hookid: bash_hook\n\nbar.py\nHello World\n\n' @@ -332,7 +304,7 @@ def test_origin_source_error_msg( repo_with_passing_hook, origin, source, expect_failure, mock_out_store_directory, cap_out, ): - args = _get_opts(origin=origin, source=source) + args = run_opts(origin=origin, source=source) ret, printed = _do_run(cap_out, repo_with_passing_hook, args) warning_msg = b'Specify both --origin and --source.' if expect_failure: @@ -350,7 +322,7 @@ def test_has_unmerged_paths(in_merge_conflict): def test_merge_conflict(cap_out, in_merge_conflict, mock_out_store_directory): - ret, printed = _do_run(cap_out, in_merge_conflict, _get_opts()) + ret, printed = _do_run(cap_out, in_merge_conflict, run_opts()) assert ret == 1 assert b'Unmerged files. Resolve before committing.' in printed @@ -363,7 +335,7 @@ def test_merge_conflict_modified( with open('dummy', 'w') as dummy_file: dummy_file.write('bar\nbaz\n') - ret, printed = _do_run(cap_out, in_merge_conflict, _get_opts()) + ret, printed = _do_run(cap_out, in_merge_conflict, run_opts()) assert ret == 1 assert b'Unmerged files. Resolve before committing.' in printed @@ -372,7 +344,7 @@ def test_merge_conflict_resolved( cap_out, in_merge_conflict, mock_out_store_directory, ): cmd_output('git', 'add', '.') - ret, printed = _do_run(cap_out, in_merge_conflict, _get_opts()) + ret, printed = _do_run(cap_out, in_merge_conflict, run_opts()) for msg in ( b'Checking merge-conflict files only.', b'Bash hook', b'Passed', ): @@ -415,7 +387,7 @@ def test_get_skips(environ, expected_output): def test_skip_hook(cap_out, repo_with_passing_hook, mock_out_store_directory): ret, printed = _do_run( - cap_out, repo_with_passing_hook, _get_opts(), {'SKIP': 'bash_hook'}, + cap_out, repo_with_passing_hook, run_opts(), {'SKIP': 'bash_hook'}, ) for msg in (b'Bash hook', b'Skipped'): assert msg in printed @@ -425,7 +397,7 @@ def test_hook_id_not_in_non_verbose_output( cap_out, repo_with_passing_hook, mock_out_store_directory, ): ret, printed = _do_run( - cap_out, repo_with_passing_hook, _get_opts(verbose=False), + cap_out, repo_with_passing_hook, run_opts(verbose=False), ) assert b'[bash_hook]' not in printed @@ -434,7 +406,7 @@ def test_hook_id_in_verbose_output( cap_out, repo_with_passing_hook, mock_out_store_directory, ): ret, printed = _do_run( - cap_out, repo_with_passing_hook, _get_opts(verbose=True), + cap_out, repo_with_passing_hook, run_opts(verbose=True), ) assert b'[bash_hook] Bash hook' in printed @@ -448,7 +420,7 @@ def test_multiple_hooks_same_id( config['repos'][0]['hooks'].append({'id': 'bash_hook'}) stage_a_file() - ret, output = _do_run(cap_out, repo_with_passing_hook, _get_opts()) + ret, output = _do_run(cap_out, repo_with_passing_hook, run_opts()) assert ret == 0 assert output.count(b'Bash hook') == 2 @@ -684,7 +656,7 @@ def modified_config_repo(repo_with_passing_hook): def test_error_with_unstaged_config( cap_out, modified_config_repo, mock_out_store_directory, ): - args = _get_opts() + args = run_opts() ret, printed = _do_run(cap_out, modified_config_repo, args) assert b'Your .pre-commit-config.yaml is unstaged.' in printed assert ret == 1 @@ -696,7 +668,7 @@ def test_error_with_unstaged_config( def test_no_unstaged_error_with_all_files_or_files( cap_out, modified_config_repo, mock_out_store_directory, opts, ): - args = _get_opts(**opts) + args = run_opts(**opts) ret, printed = _do_run(cap_out, modified_config_repo, args) assert b'Your .pre-commit-config.yaml is unstaged.' not in printed @@ -742,7 +714,7 @@ def test_pass_filenames( config['repos'][0]['hooks'][0]['args'] = hook_args stage_a_file() ret, printed = _do_run( - cap_out, repo_with_passing_hook, _get_opts(verbose=True), + cap_out, repo_with_passing_hook, run_opts(verbose=True), ) assert expected_out + b'\nHello World' in printed assert (b'foo.py' in printed) == pass_filenames @@ -758,7 +730,7 @@ def test_fail_fast( config['repos'][0]['hooks'] *= 2 stage_a_file() - ret, printed = _do_run(cap_out, repo_with_failing_hook, _get_opts()) + ret, printed = _do_run(cap_out, repo_with_failing_hook, run_opts()) # it should have only run one hook assert printed.count(b'Failing hook') == 1 diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py new file mode 100644 index 000000000..e530dee80 --- /dev/null +++ b/tests/commands/try_repo_test.py @@ -0,0 +1,71 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import re + +from pre_commit.commands.try_repo import try_repo +from pre_commit.util import cmd_output +from pre_commit.util import cwd +from testing.auto_namedtuple import auto_namedtuple +from testing.fixtures import git_dir +from testing.fixtures import make_repo +from testing.util import run_opts + + +def try_repo_opts(repo, ref=None, **kwargs): + return auto_namedtuple(repo=repo, ref=ref, **run_opts(**kwargs)._asdict()) + + +def _get_out(cap_out): + out = cap_out.get().replace('\r\n', '\n') + out = re.sub('\[INFO\].+\n', '', out) + start, using_config, config, rest = out.split('=' * 79 + '\n') + assert start == '' + assert using_config == 'Using config:\n' + return config, rest + + +def _run_try_repo(tempdir_factory, **kwargs): + repo = make_repo(tempdir_factory, 'modified_file_returns_zero_repo') + with cwd(git_dir(tempdir_factory)): + open('test-file', 'a').close() + cmd_output('git', 'add', '.') + assert not try_repo(try_repo_opts(repo, **kwargs)) + + +def test_try_repo_repo_only(cap_out, tempdir_factory): + _run_try_repo(tempdir_factory, verbose=True) + config, rest = _get_out(cap_out) + assert re.match( + '^repos:\n' + '- repo: .+\n' + ' sha: .+\n' + ' hooks:\n' + ' - id: bash_hook\n' + ' - id: bash_hook2\n' + ' - id: bash_hook3\n$', + config, + ) + assert rest == ( + '[bash_hook] Bash hook................................(no files to check)Skipped\n' # noqa + '[bash_hook2] Bash hook...................................................Passed\n' # noqa + 'hookid: bash_hook2\n' + '\n' + 'test-file\n' + '\n' + '[bash_hook3] Bash hook...............................(no files to check)Skipped\n' # noqa + ) + + +def test_try_repo_with_specific_hook(cap_out, tempdir_factory): + _run_try_repo(tempdir_factory, hook='bash_hook', verbose=True) + config, rest = _get_out(cap_out) + assert re.match( + '^repos:\n' + '- repo: .+\n' + ' sha: .+\n' + ' hooks:\n' + ' - id: bash_hook\n$', + config, + ) + assert rest == '[bash_hook] Bash hook................................(no files to check)Skipped\n' # noqa diff --git a/tests/main_test.py b/tests/main_test.py index 933b5259c..e925cfcfd 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -92,12 +92,18 @@ def test_help_other_command( @pytest.mark.parametrize('command', CMDS) -def test_install_command(command, mock_commands): +def test_all_cmds(command, mock_commands): main.main((command,)) assert getattr(mock_commands, command.replace('-', '_')).call_count == 1 assert_only_one_mock_called(mock_commands) +def test_try_repo(): + with mock.patch.object(main, 'try_repo') as patch: + main.main(('try-repo', '.')) + assert patch.call_count == 1 + + def test_help_cmd_in_empty_directory( mock_commands, tempdir_factory, diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py index 34233424e..9a0f1e591 100644 --- a/tests/make_archives_test.py +++ b/tests/make_archives_test.py @@ -6,11 +6,11 @@ import pytest +from pre_commit import git from pre_commit import make_archives from pre_commit.util import cmd_output from pre_commit.util import cwd from testing.fixtures import git_dir -from testing.util import get_head_sha from testing.util import skipif_slowtests_false @@ -23,7 +23,7 @@ def test_make_archive(tempdir_factory): cmd_output('git', 'add', '.') cmd_output('git', 'commit', '-m', 'foo') # We'll use this sha - head_sha = get_head_sha('.') + head_sha = git.head_sha('.') # And check that this file doesn't exist open('bar', 'a').close() cmd_output('git', 'add', '.') diff --git a/tests/manifest_test.py b/tests/manifest_test.py index ee1857c9a..b7603a0d7 100644 --- a/tests/manifest_test.py +++ b/tests/manifest_test.py @@ -3,16 +3,16 @@ import pytest +from pre_commit import git from pre_commit.manifest import Manifest from testing.fixtures import make_repo -from testing.util import get_head_sha @pytest.yield_fixture def manifest(store, tempdir_factory): path = make_repo(tempdir_factory, 'script_hooks_repo') - repo_path = store.clone(path, get_head_sha(path)) - yield Manifest(repo_path, path) + repo_path = store.clone(path, git.head_sha(path)) + yield Manifest(repo_path) def test_manifest_contents(manifest): @@ -62,8 +62,8 @@ def test_hooks(manifest): def test_default_python_language_version(store, tempdir_factory): path = make_repo(tempdir_factory, 'python_hooks_repo') - repo_path = store.clone(path, get_head_sha(path)) - manifest = Manifest(repo_path, path) + repo_path = store.clone(path, git.head_sha(path)) + manifest = Manifest(repo_path) # This assertion is difficult as it is version dependent, just assert # that it is *something* diff --git a/tests/store_test.py b/tests/store_test.py index 718f24d0d..deb22bb8e 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -9,13 +9,13 @@ import pytest import six +from pre_commit import git from pre_commit.store import _get_default_directory from pre_commit.store import Store from pre_commit.util import cmd_output from pre_commit.util import cwd from pre_commit.util import rmtree from testing.fixtures import git_dir -from testing.util import get_head_sha def test_our_session_fixture_works(): @@ -91,7 +91,7 @@ def test_clone(store, tempdir_factory, log_info_mock): path = git_dir(tempdir_factory) with cwd(path): cmd_output('git', 'commit', '--allow-empty', '-m', 'foo') - sha = get_head_sha(path) + sha = git.head_sha(path) cmd_output('git', 'commit', '--allow-empty', '-m', 'bar') ret = store.clone(path, sha) @@ -107,7 +107,7 @@ def test_clone(store, tempdir_factory, log_info_mock): _, dirname = os.path.split(ret) assert dirname.startswith('repo') # Should be checked out to the sha we specified - assert get_head_sha(ret) == sha + assert git.head_sha(ret) == sha # Assert there's an entry in the sqlite db for this with sqlite3.connect(store.db_path) as db: From 2a984c37463da9f712cd89c5b4c1c9889d3b200f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 8 Oct 2017 15:07:48 -0700 Subject: [PATCH 092/544] v1.3.0 --- CHANGELOG.md | 16 ++++++++++++++++ setup.py | 3 +-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a6160db1..f07185c70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +1.3.0 +===== + +### Features +- Add `pre-commit try-repo` commands + - The new `try-repo` takes a repo and will run the hooks configured in + that hook repository. + - An example invocation: + `pre-commit try-repo https://github.com/pre-commit/pre-commit-hooks` + - `pre-commit try-repo` can also take all the same arguments as + `pre-commit run`. + - It can be used to try out a repository without needing to configure it. + - It can also be used to test a hook repository while developing it. + - #589 issue by @sverhagen. + - #633 PR by @asottile. + 1.2.0 ===== diff --git a/setup.py b/setup.py index fbb2397c5..0c1e83572 100644 --- a/setup.py +++ b/setup.py @@ -9,12 +9,11 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.2.0', + version='1.3.0', author='Anthony Sottile', author_email='asottile@umich.edu', - platforms='linux', classifiers=[ 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 2', From 10912fa03ee974c27e9f377fbbf40a7380984bed Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 20 Oct 2017 13:04:33 -0700 Subject: [PATCH 093/544] Lazily install repositories --- pre_commit/commands/run.py | 37 +++++++++++++++---------------------- pre_commit/runner.py | 5 +---- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 74bff891e..4cbc99bbe 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -197,12 +197,6 @@ def _run_hooks(config, repo_hooks, args, environ): return retval -def get_repo_hooks(runner): - for repo in runner.repositories: - for _, hook in repo.hooks: - yield (repo, hook) - - def _has_unmerged_paths(): _, stdout, _ = cmd_output('git', 'ls-files', '--unmerged') return bool(stdout.strip()) @@ -245,21 +239,20 @@ def run(runner, args, environ=os.environ): ctx = staged_files_only(runner.store.directory) with ctx: - repo_hooks = list(get_repo_hooks(runner)) - - if args.hook: - repo_hooks = [ - (repo, hook) for repo, hook in repo_hooks - if hook['id'] == args.hook - ] - if not repo_hooks: - output.write_line('No hook with id `{}`'.format(args.hook)) - return 1 - - # Filter hooks for stages - repo_hooks = [ - (repo, hook) for repo, hook in repo_hooks - if not hook['stages'] or args.hook_stage in hook['stages'] - ] + repo_hooks = [] + for repo in runner.repositories: + for _, hook in repo.hooks: + if ( + (not args.hook or hook['id'] == args.hook) and + not hook['stages'] or args.hook_stage in hook['stages'] + ): + repo_hooks.append((repo, hook)) + + if args.hook and not repo_hooks: + output.write_line('No hook with id `{}`'.format(args.hook)) + return 1 + + for repo in {repo for repo, _ in repo_hooks}: + repo.require_installed() return _run_hooks(runner.config, repo_hooks, args, environ) diff --git a/pre_commit/runner.py b/pre_commit/runner.py index 1983bab84..420c62df9 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -46,10 +46,7 @@ def config(self): def repositories(self): """Returns a tuple of the configured repositories.""" repos = self.config['repos'] - repos = tuple(Repository.create(x, self.store) for x in repos) - for repo in repos: - repo.require_installed() - return repos + return tuple(Repository.create(x, self.store) for x in repos) def get_hook_path(self, hook_type): return os.path.join(self.git_dir, 'hooks', hook_type) From ac21235b847ca01f3a437df42f4410c394a48153 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 20 Oct 2017 14:15:15 -0700 Subject: [PATCH 094/544] Remove unused logger --- pre_commit/manifest.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pre_commit/manifest.py b/pre_commit/manifest.py index 99d839308..10e312fbf 100644 --- a/pre_commit/manifest.py +++ b/pre_commit/manifest.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -import logging import os.path from cached_property import cached_property @@ -10,9 +9,6 @@ from pre_commit.languages.all import languages -logger = logging.getLogger('pre_commit') - - class Manifest(object): def __init__(self, repo_path): self.repo_path = repo_path From 88c676a7c18d9ebe4dbdbd8d1851ba2c98b10dae Mon Sep 17 00:00:00 2001 From: Paul Hooijenga Date: Sun, 22 Oct 2017 16:40:19 +0200 Subject: [PATCH 095/544] Add support for meta hooks --- pre_commit/clientlib.py | 9 +++++++- pre_commit/repository.py | 43 ++++++++++++++++++++++++++++++++++++++ pre_commit/schema.py | 11 ++++++++++ tests/commands/run_test.py | 25 ++++++++++++++++++++++ tests/repository_test.py | 12 +++++++++++ tests/schema_test.py | 31 +++++++++++++++++++++++++++ 6 files changed, 130 insertions(+), 1 deletion(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 11750b741..3c086cb94 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -98,6 +98,8 @@ def validate_manifest_main(argv=None): _LOCAL_SENTINEL = 'local' +_META_SENTINEL = 'meta' + CONFIG_HOOK_DICT = schema.Map( 'Hook', 'id', @@ -121,7 +123,8 @@ def validate_manifest_main(argv=None): schema.Conditional( 'sha', schema.check_string, - condition_key='repo', condition_value=schema.Not(_LOCAL_SENTINEL), + condition_key='repo', + condition_value=schema.NotIn((_LOCAL_SENTINEL, _META_SENTINEL)), ensure_absent=True, ), ) @@ -138,6 +141,10 @@ def is_local_repo(repo_entry): return repo_entry['repo'] == _LOCAL_SENTINEL +def is_meta_repo(repo_entry): + return repo_entry['repo'] == _META_SENTINEL + + class InvalidConfigError(FatalError): pass diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 4d7f0a5a0..b0858ba9e 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -5,6 +5,7 @@ import logging import os import shutil +import sys from collections import defaultdict import pkg_resources @@ -14,6 +15,7 @@ from pre_commit import five from pre_commit import git from pre_commit.clientlib import is_local_repo +from pre_commit.clientlib import is_meta_repo from pre_commit.clientlib import MANIFEST_HOOK_DICT from pre_commit.languages.all import languages from pre_commit.languages.helpers import environment_dir @@ -128,6 +130,8 @@ def __init__(self, repo_config, store): def create(cls, config, store): if is_local_repo(config): return LocalRepository(config, store) + elif is_meta_repo(config): + return MetaRepository(config, store) else: return cls(config, store) @@ -242,6 +246,45 @@ def _venvs(self): return tuple(ret) +class MetaRepository(LocalRepository): + meta_hooks = { + 'test-hook': { + 'name': 'Test Hook', + 'files': '', + 'language': 'system', + 'entry': 'echo "Hello World!"', + 'always_run': True, + }, + } + + @cached_property + def hooks(self): + for hook in self.repo_config['hooks']: + if hook['id'] not in self.meta_hooks: + logger.error( + '`{}` is not a valid meta hook. ' + 'Typo? Perhaps it is introduced in a newer version? ' + 'Often `pre-commit autoupdate` fixes this.'.format( + hook['id'], + ), + ) + exit(1) + + return tuple( + ( + hook['id'], + apply_defaults( + validate( + dict(self.meta_hooks[hook['id']], **hook), + MANIFEST_HOOK_DICT, + ), + MANIFEST_HOOK_DICT, + ), + ) + for hook in self.repo_config['hooks'] + ) + + class _UniqueList(list): def __init__(self): self._set = set() diff --git a/pre_commit/schema.py b/pre_commit/schema.py index e20f74ccb..e85c23039 100644 --- a/pre_commit/schema.py +++ b/pre_commit/schema.py @@ -101,6 +101,9 @@ def _check_conditional(self, dct): if isinstance(self.condition_value, Not): op = 'is' cond_val = self.condition_value.val + elif isinstance(self.condition_value, NotIn): + op = 'is any of' + cond_val = self.condition_value.values else: op = 'is not' cond_val = self.condition_value @@ -206,6 +209,14 @@ def __eq__(self, other): return other is not MISSING and other != self.val +class NotIn(object): + def __init__(self, values): + self.values = values + + def __eq__(self, other): + return other is not MISSING and other not in self.values + + def check_any(_): pass diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index d6812ae5a..e52716fa2 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -645,6 +645,31 @@ def test_local_hook_fails( ) +def test_meta_hook_passes( + cap_out, repo_with_passing_hook, mock_out_store_directory, +): + config = OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'test-hook'), + )), + ), + ), + )) + add_config_to_repo(repo_with_passing_hook, config) + + _test_run( + cap_out, + repo_with_passing_hook, + opts={'verbose': True}, + expected_outputs=[b'Hello World!'], + expected_ret=0, + stage=False, + ) + + @pytest.yield_fixture def modified_config_repo(repo_with_passing_hook): with modify_config(repo_with_passing_hook, commit=False) as config: diff --git a/tests/repository_test.py b/tests/repository_test.py index 37a609baf..263ce1ea5 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -709,6 +709,18 @@ def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): ) +def test_meta_hook_not_present(store, fake_log_handler): + config = {'repo': 'meta', 'hooks': [{'id': 'i-dont-exist'}]} + repo = Repository.create(config, store) + with pytest.raises(SystemExit): + repo.require_installed() + assert fake_log_handler.handle.call_args[0][0].msg == ( + '`i-dont-exist` is not a valid meta hook. ' + 'Typo? Perhaps it is introduced in a newer version? ' + 'Often `pre-commit autoupdate` fixes this.' + ) + + def test_too_new_version(tempdir_factory, store, fake_log_handler): path = make_repo(tempdir_factory, 'script_hooks_repo') with modify_manifest(path) as manifest: diff --git a/tests/schema_test.py b/tests/schema_test.py index c2ecf0faa..06f28e769 100644 --- a/tests/schema_test.py +++ b/tests/schema_test.py @@ -19,6 +19,7 @@ from pre_commit.schema import Map from pre_commit.schema import MISSING from pre_commit.schema import Not +from pre_commit.schema import NotIn from pre_commit.schema import Optional from pre_commit.schema import OptionalNoDefault from pre_commit.schema import remove_defaults @@ -107,6 +108,16 @@ def test_not(val, expected): assert (compared == val) is expected +@pytest.mark.parametrize( + ('values', 'expected'), + (('bar', True), ('foo', False), (MISSING, False)), +) +def test_not_in(values, expected): + compared = NotIn(('baz', 'foo')) + assert (values == compared) is expected + assert (compared == values) is expected + + trivial_array_schema = Array(Map('foo', 'id')) @@ -196,6 +207,13 @@ def test_optional_key_missing(schema): condition_key='key', condition_value=Not(True), ensure_absent=True, ), ) +map_conditional_absent_not_in = Map( + 'foo', 'key', + Conditional( + 'key2', check_bool, + condition_key='key', condition_value=NotIn((1, 2)), ensure_absent=True, + ), +) @pytest.mark.parametrize('schema', (map_conditional, map_conditional_not)) @@ -248,6 +266,19 @@ def test_ensure_absent_conditional_not(): ) +def test_ensure_absent_conditional_not_in(): + with pytest.raises(ValidationError) as excinfo: + validate({'key': 1, 'key2': True}, map_conditional_absent_not_in) + _assert_exception_trace( + excinfo.value, + ( + 'At foo(key=1)', + 'Expected key2 to be absent when key is any of (1, 2), ' + 'found key2: True', + ), + ) + + def test_no_error_conditional_absent(): validate({}, map_conditional_absent) validate({}, map_conditional_absent_not) From f0cf940cb531ea64bef91d609923015c74e9e9cd Mon Sep 17 00:00:00 2001 From: Jimmi Dyson Date: Mon, 23 Oct 2017 11:08:06 +0100 Subject: [PATCH 096/544] Add selinux labelling option to docker_image hook type --- pre_commit/languages/docker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 3dddf6189..0d063cb97 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -82,7 +82,7 @@ def docker_cmd(): 'docker', 'run', '--rm', '-u', '{}:{}'.format(os.getuid(), os.getgid()), - '-v', '{}:/src:rw'.format(os.getcwd()), + '-v', '{}:/src:rw,Z'.format(os.getcwd()), '--workdir', '/src', ) From 8df11ee7aaa53c6055d5b22bdd8ef82afb5be6d7 Mon Sep 17 00:00:00 2001 From: Paul Hooijenga Date: Mon, 23 Oct 2017 14:29:08 +0200 Subject: [PATCH 097/544] Implement check-useless-excludes meta hook --- pre_commit/meta_hooks/__init__.py | 0 .../meta_hooks/check_useless_excludes.py | 41 +++++++++++ pre_commit/repository.py | 14 ++-- tests/commands/run_test.py | 71 ++++++++++++++++++- 4 files changed, 118 insertions(+), 8 deletions(-) create mode 100644 pre_commit/meta_hooks/__init__.py create mode 100644 pre_commit/meta_hooks/check_useless_excludes.py diff --git a/pre_commit/meta_hooks/__init__.py b/pre_commit/meta_hooks/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py new file mode 100644 index 000000000..8e891bc1d --- /dev/null +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -0,0 +1,41 @@ +import re +import sys + +import pre_commit.constants as C +from pre_commit.clientlib import load_config +from pre_commit.git import get_all_files + + +def exclude_matches_any(filenames, include, exclude): + include_re, exclude_re = re.compile(include), re.compile(exclude) + for filename in filenames: + if include_re.search(filename) and exclude_re.search(filename): + return True + return False + + +def check_useless_excludes(config_file=None): + config = load_config(config_file or C.CONFIG_FILE) + files = get_all_files() + useless_excludes = False + + exclude = config.get('exclude') + if exclude != '^$' and not exclude_matches_any(files, '', exclude): + print('The global exclude pattern does not match any files') + useless_excludes = True + + for repo in config['repos']: + for hook in repo['hooks']: + include, exclude = hook.get('files', ''), hook.get('exclude') + if exclude and not exclude_matches_any(files, include, exclude): + print( + 'The exclude pattern for {} does not match any files' + .format(hook['id']) + ) + useless_excludes = True + + return useless_excludes + + +if __name__ == '__main__': + sys.exit(check_useless_excludes()) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index b0858ba9e..cb53fc854 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -4,6 +4,7 @@ import json import logging import os +import pipes import shutil import sys from collections import defaultdict @@ -247,13 +248,16 @@ def _venvs(self): class MetaRepository(LocalRepository): + # Note: the hook `entry` is passed through `shlex.split()` by the command + # runner, so to prevent issues with spaces and backslashes (on Windows) it + # must be quoted here. meta_hooks = { - 'test-hook': { - 'name': 'Test Hook', - 'files': '', + 'check-useless-excludes': { + 'name': 'Check for useless excludes', + 'files': '.pre-commit-config.yaml', 'language': 'system', - 'entry': 'echo "Hello World!"', - 'always_run': True, + 'entry': pipes.quote(sys.executable), + 'args': ['-m', 'pre_commit.meta_hooks.check_useless_excludes'], }, } diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index e52716fa2..27fa9eea0 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -653,7 +653,7 @@ def test_meta_hook_passes( ( 'hooks', ( OrderedDict(( - ('id', 'test-hook'), + ('id', 'check-useless-excludes'), )), ), ), @@ -663,13 +663,78 @@ def test_meta_hook_passes( _test_run( cap_out, repo_with_passing_hook, - opts={'verbose': True}, - expected_outputs=[b'Hello World!'], + opts={}, + expected_outputs=[b'Check for useless excludes'], expected_ret=0, stage=False, ) +def test_useless_exclude_global( + cap_out, repo_with_passing_hook, mock_out_store_directory, +): + config = OrderedDict(( + ('exclude', 'foo'), + ( + 'repos', [ + OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'check-useless-excludes'), + )), + ), + ), + )), + ], + ), + )) + add_config_to_repo(repo_with_passing_hook, config) + + _test_run( + cap_out, + repo_with_passing_hook, + opts={'all_files': True}, + expected_outputs=[ + b'Check for useless excludes', + b'The global exclude pattern does not match any files', + ], + expected_ret=1, + stage=False, + ) + + +def test_useless_exclude_for_hook( + cap_out, repo_with_passing_hook, mock_out_store_directory, +): + config = OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'check-useless-excludes'), + ('exclude', 'foo'), + )), + ), + ), + )) + add_config_to_repo(repo_with_passing_hook, config) + + _test_run( + cap_out, + repo_with_passing_hook, + opts={'all_files': True}, + expected_outputs=[ + b'Check for useless excludes', + b'The exclude pattern for check-useless-excludes ' + b'does not match any files', + ], + expected_ret=1, + stage=False, + ) + + @pytest.yield_fixture def modified_config_repo(repo_with_passing_hook): with modify_config(repo_with_passing_hook, commit=False) as config: From 8a0dd01c7e985970e62d3ff57841b0b0c485b8a3 Mon Sep 17 00:00:00 2001 From: Paul Hooijenga Date: Wed, 25 Oct 2017 09:35:39 +0200 Subject: [PATCH 098/544] Implement check-files-matches-any meta hook --- .../meta_hooks/check_files_matches_any.py | 36 +++++++++++++++++++ pre_commit/repository.py | 7 ++++ tests/commands/run_test.py | 33 +++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 pre_commit/meta_hooks/check_files_matches_any.py diff --git a/pre_commit/meta_hooks/check_files_matches_any.py b/pre_commit/meta_hooks/check_files_matches_any.py new file mode 100644 index 000000000..88b4806fd --- /dev/null +++ b/pre_commit/meta_hooks/check_files_matches_any.py @@ -0,0 +1,36 @@ +import re +import sys + +import pre_commit.constants as C +from pre_commit.clientlib import load_config +from pre_commit.git import get_all_files + + +def files_matches_any(filenames, include): + include_re = re.compile(include) + for filename in filenames: + if include_re.search(filename): + return True + return False + + +def check_files_matches_any(config_file=None): + config = load_config(config_file or C.CONFIG_FILE) + files = get_all_files() + files_not_matched = False + + for repo in config['repos']: + for hook in repo['hooks']: + include = hook.get('files', '') + if include and not files_matches_any(files, include): + print( + 'The files pattern for {} does not match any files' + .format(hook['id']) + ) + files_not_matched = True + + return files_not_matched + + +if __name__ == '__main__': + sys.exit(check_files_matches_any()) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index cb53fc854..45389cb46 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -259,6 +259,13 @@ class MetaRepository(LocalRepository): 'entry': pipes.quote(sys.executable), 'args': ['-m', 'pre_commit.meta_hooks.check_useless_excludes'], }, + 'check-files-matches-any': { + 'name': 'Check hooks match any files', + 'files': '.pre-commit-config.yaml', + 'language': 'system', + 'entry': pipes.quote(sys.executable), + 'args': ['-m', 'pre_commit.meta_hooks.check_files_matches_any'], + }, } @cached_property diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 27fa9eea0..24771d22f 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -735,6 +735,39 @@ def test_useless_exclude_for_hook( ) +def test_files_match_any( + cap_out, repo_with_passing_hook, mock_out_store_directory, +): + config = OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'check-files-matches-any'), + )), + OrderedDict(( + ('id', 'check-useless-excludes'), + ('files', 'foo'), + )), + ), + ), + )) + add_config_to_repo(repo_with_passing_hook, config) + + _test_run( + cap_out, + repo_with_passing_hook, + opts={'all_files': True}, + expected_outputs=[ + b'Check hooks match any files', + b'The files pattern for check-useless-excludes ' + b'does not match any files', + ], + expected_ret=1, + stage=False, + ) + + @pytest.yield_fixture def modified_config_repo(repo_with_passing_hook): with modify_config(repo_with_passing_hook, commit=False) as config: From 6a0fe9889b44b863ce9c0ded88251980cfe9e6fc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 26 Oct 2017 14:26:33 -0700 Subject: [PATCH 099/544] Apply interpreter version defaulting to local hooks too --- pre_commit/manifest.py | 9 +------- pre_commit/repository.py | 45 +++++++++++++++++++++------------------- tests/manifest_test.py | 10 --------- tests/repository_test.py | 12 +++++++++++ 4 files changed, 37 insertions(+), 39 deletions(-) diff --git a/pre_commit/manifest.py b/pre_commit/manifest.py index 10e312fbf..c9caa43bc 100644 --- a/pre_commit/manifest.py +++ b/pre_commit/manifest.py @@ -6,7 +6,6 @@ import pre_commit.constants as C from pre_commit.clientlib import load_manifest -from pre_commit.languages.all import languages class Manifest(object): @@ -19,10 +18,4 @@ def manifest_contents(self): @cached_property def hooks(self): - ret = {} - for hook in self.manifest_contents: - if hook['language_version'] == 'default': - language = languages[hook['language']] - hook['language_version'] = language.get_default_version() - ret[hook['id']] = hook - return ret + return {hook['id']: hook for hook in self.manifest_contents} diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 4d7f0a5a0..b8aa1ff07 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -102,20 +102,27 @@ def _need_installed(): _write_state(cmd_runner, venv, state) -def _validate_minimum_version(hook): - hook_version = pkg_resources.parse_version( - hook['minimum_pre_commit_version'], - ) - if hook_version > C.VERSION_PARSED: +def _hook(*hook_dicts): + ret, rest = dict(hook_dicts[0]), hook_dicts[1:] + for dct in rest: + ret.update(dct) + + version = pkg_resources.parse_version(ret['minimum_pre_commit_version']) + if version > C.VERSION_PARSED: logger.error( - 'The hook `{}` requires pre-commit version {} but ' - 'version {} is installed. ' + 'The hook `{}` requires pre-commit version {} but version {} ' + 'is installed. ' 'Perhaps run `pip install --upgrade pre-commit`.'.format( - hook['id'], hook_version, C.VERSION_PARSED, + ret['id'], version, C.VERSION_PARSED, ), ) exit(1) - return hook + + if ret['language_version'] == 'default': + language = languages[ret['language']] + ret['language_version'] = language.get_default_version() + + return ret class Repository(object): @@ -161,10 +168,8 @@ def hooks(self): ) exit(1) - _validate_minimum_version(self.manifest.hooks[hook['id']]) - return tuple( - (hook['id'], dict(self.manifest.hooks[hook['id']], **hook)) + (hook['id'], _hook(self.manifest.hooks[hook['id']], hook)) for hook in self.repo_config['hooks'] ) @@ -215,16 +220,14 @@ def manifest(self): @cached_property def hooks(self): + def _from_manifest_dct(dct): + dct = validate(dct, MANIFEST_HOOK_DICT) + dct = apply_defaults(dct, MANIFEST_HOOK_DICT) + dct = _hook(dct) + return dct + return tuple( - ( - hook['id'], - _validate_minimum_version( - apply_defaults( - validate(hook, MANIFEST_HOOK_DICT), - MANIFEST_HOOK_DICT, - ), - ), - ) + (hook['id'], _from_manifest_dct(hook)) for hook in self.repo_config['hooks'] ) diff --git a/tests/manifest_test.py b/tests/manifest_test.py index b7603a0d7..85a3e3c6e 100644 --- a/tests/manifest_test.py +++ b/tests/manifest_test.py @@ -58,13 +58,3 @@ def test_hooks(manifest): 'types': ['file'], 'exclude_types': [], } - - -def test_default_python_language_version(store, tempdir_factory): - path = make_repo(tempdir_factory, 'python_hooks_repo') - repo_path = store.clone(path, git.head_sha(path)) - manifest = Manifest(repo_path) - - # This assertion is difficult as it is version dependent, just assert - # that it is *something* - assert manifest.hooks['foo']['language_version'] != 'default' diff --git a/tests/repository_test.py b/tests/repository_test.py index 37a609baf..62a3af8b1 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -71,6 +71,16 @@ def test_python_hook(tempdir_factory, store): ) +@pytest.mark.integration +def test_python_hook_default_version(tempdir_factory, store): + # make sure that this continues to work for platforms where default + # language detection does not work + with mock.patch.object( + python, 'get_default_version', return_value='default', + ): + test_python_hook(tempdir_factory, store) + + @pytest.mark.integration def test_python_hook_args_with_spaces(tempdir_factory, store): _test_hook_repo( @@ -690,6 +700,8 @@ def test_local_python_repo(store): config = {'repo': 'local', 'hooks': hooks} repo = Repository.create(config, store) (_, hook), = repo.hooks + # language_version should have been adjusted to the interpreter version + assert hook['language_version'] != 'default' ret = repo.run_hook(hook, ('filename',)) assert ret[0] == 0 assert _norm_out(ret[1]) == b"['filename']\nHello World\n" From 84b1ba520d42b28c6c5f24a6fe29f27baaa33576 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 26 Oct 2017 16:08:40 -0700 Subject: [PATCH 100/544] Remove Manifest, no longer a useful abstraction --- pre_commit/commands/autoupdate.py | 2 +- pre_commit/commands/try_repo.py | 8 +++-- pre_commit/manifest.py | 21 ----------- pre_commit/repository.py | 11 +++--- tests/manifest_test.py | 60 ------------------------------- tests/repository_test.py | 26 ++++++++++++++ 6 files changed, 38 insertions(+), 90 deletions(-) delete mode 100644 pre_commit/manifest.py delete mode 100644 tests/manifest_test.py diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 4dce674f3..a80c6b403 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -57,7 +57,7 @@ def _update_repo(repo_config, runner, tags_only): # See if any of our hooks were deleted with the new commits hooks = {hook['id'] for hook in repo.repo_config['hooks']} - hooks_missing = hooks - (hooks & set(new_repo.manifest.hooks)) + hooks_missing = hooks - (hooks & set(new_repo.manifest_hooks)) if hooks_missing: raise RepositoryCannotBeUpdatedError( 'Cannot update because the tip of master is missing these hooks:\n' diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index 2e1933d81..4c8258231 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -9,8 +9,8 @@ import pre_commit.constants as C from pre_commit import git from pre_commit import output +from pre_commit.clientlib import load_manifest from pre_commit.commands.run import run -from pre_commit.manifest import Manifest from pre_commit.runner import Runner from pre_commit.store import Store from pre_commit.util import tmpdir @@ -23,8 +23,10 @@ def try_repo(args): if args.hook: hooks = [{'id': args.hook}] else: - manifest = Manifest(Store(tempdir).clone(args.repo, ref)) - hooks = [{'id': hook_id} for hook_id in sorted(manifest.hooks)] + repo_path = Store(tempdir).clone(args.repo, ref) + manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) + manifest = sorted(manifest, key=lambda hook: hook['id']) + hooks = [{'id': hook['id']} for hook in manifest] items = (('repo', args.repo), ('sha', ref), ('hooks', hooks)) config = {'repos': [collections.OrderedDict(items)]} diff --git a/pre_commit/manifest.py b/pre_commit/manifest.py deleted file mode 100644 index c9caa43bc..000000000 --- a/pre_commit/manifest.py +++ /dev/null @@ -1,21 +0,0 @@ -from __future__ import unicode_literals - -import os.path - -from cached_property import cached_property - -import pre_commit.constants as C -from pre_commit.clientlib import load_manifest - - -class Manifest(object): - def __init__(self, repo_path): - self.repo_path = repo_path - - @cached_property - def manifest_contents(self): - return load_manifest(os.path.join(self.repo_path, C.MANIFEST_FILE)) - - @cached_property - def hooks(self): - return {hook['id']: hook for hook in self.manifest_contents} diff --git a/pre_commit/repository.py b/pre_commit/repository.py index b8aa1ff07..d7af2e214 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -14,10 +14,10 @@ from pre_commit import five from pre_commit import git from pre_commit.clientlib import is_local_repo +from pre_commit.clientlib import load_manifest from pre_commit.clientlib import MANIFEST_HOOK_DICT from pre_commit.languages.all import languages from pre_commit.languages.helpers import environment_dir -from pre_commit.manifest import Manifest from pre_commit.prefixed_command_runner import PrefixedCommandRunner from pre_commit.schema import apply_defaults from pre_commit.schema import validate @@ -152,13 +152,14 @@ def _cmd_runner_from_deps(self, language_name, deps): return self._cmd_runner @cached_property - def manifest(self): - return Manifest(self._repo_path) + def manifest_hooks(self): + manifest_path = os.path.join(self._repo_path, C.MANIFEST_FILE) + return {hook['id']: hook for hook in load_manifest(manifest_path)} @cached_property def hooks(self): for hook in self.repo_config['hooks']: - if hook['id'] not in self.manifest.hooks: + if hook['id'] not in self.manifest_hooks: logger.error( '`{}` is not present in repository {}. ' 'Typo? Perhaps it is introduced in a newer version? ' @@ -169,7 +170,7 @@ def hooks(self): exit(1) return tuple( - (hook['id'], _hook(self.manifest.hooks[hook['id']], hook)) + (hook['id'], _hook(self.manifest_hooks[hook['id']], hook)) for hook in self.repo_config['hooks'] ) diff --git a/tests/manifest_test.py b/tests/manifest_test.py deleted file mode 100644 index 85a3e3c6e..000000000 --- a/tests/manifest_test.py +++ /dev/null @@ -1,60 +0,0 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - -import pytest - -from pre_commit import git -from pre_commit.manifest import Manifest -from testing.fixtures import make_repo - - -@pytest.yield_fixture -def manifest(store, tempdir_factory): - path = make_repo(tempdir_factory, 'script_hooks_repo') - repo_path = store.clone(path, git.head_sha(path)) - yield Manifest(repo_path) - - -def test_manifest_contents(manifest): - # Should just retrieve the manifest contents - assert manifest.manifest_contents == [{ - 'always_run': False, - 'additional_dependencies': [], - 'args': [], - 'description': '', - 'entry': 'bin/hook.sh', - 'exclude': '^$', - 'files': '', - 'id': 'bash_hook', - 'language': 'script', - 'language_version': 'default', - 'log_file': '', - 'minimum_pre_commit_version': '0', - 'name': 'Bash hook', - 'pass_filenames': True, - 'stages': [], - 'types': ['file'], - 'exclude_types': [], - }] - - -def test_hooks(manifest): - assert manifest.hooks['bash_hook'] == { - 'always_run': False, - 'additional_dependencies': [], - 'args': [], - 'description': '', - 'entry': 'bin/hook.sh', - 'exclude': '^$', - 'files': '', - 'id': 'bash_hook', - 'language': 'script', - 'language_version': 'default', - 'log_file': '', - 'minimum_pre_commit_version': '0', - 'name': 'Bash hook', - 'pass_filenames': True, - 'stages': [], - 'types': ['file'], - 'exclude_types': [], - } diff --git a/tests/repository_test.py b/tests/repository_test.py index 62a3af8b1..fee76d87f 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -746,3 +746,29 @@ def test_versions_ok(tempdir_factory, store, version): config = make_config_from_repo(path) # Should succeed Repository.create(config, store).require_installed() + + +def test_manifest_hooks(tempdir_factory, store): + path = make_repo(tempdir_factory, 'script_hooks_repo') + config = make_config_from_repo(path) + repo = Repository.create(config, store) + + assert repo.manifest_hooks['bash_hook'] == { + 'always_run': False, + 'additional_dependencies': [], + 'args': [], + 'description': '', + 'entry': 'bin/hook.sh', + 'exclude': '^$', + 'files': '', + 'id': 'bash_hook', + 'language': 'script', + 'language_version': 'default', + 'log_file': '', + 'minimum_pre_commit_version': '0', + 'name': 'Bash hook', + 'pass_filenames': True, + 'stages': [], + 'types': ['file'], + 'exclude_types': [], + } From a0a8fc15ffe9802e608ebfc25b3eece88625e392 Mon Sep 17 00:00:00 2001 From: Paul Hooijenga Date: Fri, 27 Oct 2017 13:30:36 +0200 Subject: [PATCH 101/544] Make Not and NotIn namedtuples --- pre_commit/clientlib.py | 2 +- pre_commit/schema.py | 11 ++++------- tests/schema_test.py | 4 ++-- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 3c086cb94..c94691a58 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -124,7 +124,7 @@ def validate_manifest_main(argv=None): schema.Conditional( 'sha', schema.check_string, condition_key='repo', - condition_value=schema.NotIn((_LOCAL_SENTINEL, _META_SENTINEL)), + condition_value=schema.NotIn(_LOCAL_SENTINEL, _META_SENTINEL), ensure_absent=True, ), ) diff --git a/pre_commit/schema.py b/pre_commit/schema.py index e85c23039..89e1bcfc5 100644 --- a/pre_commit/schema.py +++ b/pre_commit/schema.py @@ -201,17 +201,14 @@ def remove_defaults(self, v): return [remove_defaults(val, self.of) for val in v] -class Not(object): - def __init__(self, val): - self.val = val - +class Not(collections.namedtuple('Not', ('val',))): def __eq__(self, other): return other is not MISSING and other != self.val -class NotIn(object): - def __init__(self, values): - self.values = values +class NotIn(collections.namedtuple('NotIn', ('values',))): + def __new__(cls, *values): + return super(NotIn, cls).__new__(cls, values=values) def __eq__(self, other): return other is not MISSING and other not in self.values diff --git a/tests/schema_test.py b/tests/schema_test.py index 06f28e769..565f7e174 100644 --- a/tests/schema_test.py +++ b/tests/schema_test.py @@ -113,7 +113,7 @@ def test_not(val, expected): (('bar', True), ('foo', False), (MISSING, False)), ) def test_not_in(values, expected): - compared = NotIn(('baz', 'foo')) + compared = NotIn('baz', 'foo') assert (values == compared) is expected assert (compared == values) is expected @@ -211,7 +211,7 @@ def test_optional_key_missing(schema): 'foo', 'key', Conditional( 'key2', check_bool, - condition_key='key', condition_value=NotIn((1, 2)), ensure_absent=True, + condition_key='key', condition_value=NotIn(1, 2), ensure_absent=True, ), ) From 9db827ef9d9e1dfcea5ecabfeb74b9f34fac9926 Mon Sep 17 00:00:00 2001 From: Paul Hooijenga Date: Sat, 28 Oct 2017 13:59:11 +0200 Subject: [PATCH 102/544] Refactor meta hooks --- .../meta_hooks/check_files_matches_any.py | 52 +++---- .../meta_hooks/check_useless_excludes.py | 30 +++- pre_commit/repository.py | 72 +++++----- tests/commands/run_test.py | 98 ------------- tests/meta_hooks/__init__.py | 0 tests/meta_hooks/hook_matches_any_test.py | 130 ++++++++++++++++++ tests/meta_hooks/useless_excludes_test.py | 107 ++++++++++++++ tests/repository_test.py | 2 +- 8 files changed, 328 insertions(+), 163 deletions(-) create mode 100644 tests/meta_hooks/__init__.py create mode 100644 tests/meta_hooks/hook_matches_any_test.py create mode 100644 tests/meta_hooks/useless_excludes_test.py diff --git a/pre_commit/meta_hooks/check_files_matches_any.py b/pre_commit/meta_hooks/check_files_matches_any.py index 88b4806fd..d253939e6 100644 --- a/pre_commit/meta_hooks/check_files_matches_any.py +++ b/pre_commit/meta_hooks/check_files_matches_any.py @@ -1,36 +1,40 @@ -import re -import sys +import argparse import pre_commit.constants as C -from pre_commit.clientlib import load_config +from pre_commit.commands.run import _filter_by_include_exclude +from pre_commit.commands.run import _filter_by_types from pre_commit.git import get_all_files +from pre_commit.runner import Runner -def files_matches_any(filenames, include): - include_re = re.compile(include) - for filename in filenames: - if include_re.search(filename): - return True - return False +def check_all_hooks_match_files(config_file): + runner = Runner.create(config_file) + files = get_all_files() + files_matched = True + for repo in runner.repositories: + for hook_id, hook in repo.hooks: + include, exclude = hook['files'], hook['exclude'] + filtered = _filter_by_include_exclude(files, include, exclude) + types, exclude_types = hook['types'], hook['exclude_types'] + filtered = _filter_by_types(filtered, types, exclude_types) + if not filtered: + print('{} does not apply to this repository'.format(hook_id)) + files_matched = False + + return files_matched -def check_files_matches_any(config_file=None): - config = load_config(config_file or C.CONFIG_FILE) - files = get_all_files() - files_not_matched = False - for repo in config['repos']: - for hook in repo['hooks']: - include = hook.get('files', '') - if include and not files_matches_any(files, include): - print( - 'The files pattern for {} does not match any files' - .format(hook['id']) - ) - files_not_matched = True +def main(argv=None): + parser = argparse.ArgumentParser() + parser.add_argument('filenames', nargs='*', default=[C.CONFIG_FILE]) + args = parser.parse_args(argv) - return files_not_matched + retv = 0 + for filename in args.filenames: + retv |= not check_all_hooks_match_files(filename) + return retv if __name__ == '__main__': - sys.exit(check_files_matches_any()) + exit(main()) diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 8e891bc1d..89448d786 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -1,5 +1,7 @@ +from __future__ import print_function + +import argparse import re -import sys import pre_commit.constants as C from pre_commit.clientlib import load_config @@ -14,14 +16,17 @@ def exclude_matches_any(filenames, include, exclude): return False -def check_useless_excludes(config_file=None): - config = load_config(config_file or C.CONFIG_FILE) +def check_useless_excludes(config_file): + config = load_config(config_file) files = get_all_files() useless_excludes = False exclude = config.get('exclude') if exclude != '^$' and not exclude_matches_any(files, '', exclude): - print('The global exclude pattern does not match any files') + print( + 'The global exclude pattern {!r} does not match any files' + .format(exclude), + ) useless_excludes = True for repo in config['repos']: @@ -29,13 +34,24 @@ def check_useless_excludes(config_file=None): include, exclude = hook.get('files', ''), hook.get('exclude') if exclude and not exclude_matches_any(files, include, exclude): print( - 'The exclude pattern for {} does not match any files' - .format(hook['id']) + 'The exclude pattern {!r} for {} does not match any files' + .format(exclude, hook['id']), ) useless_excludes = True return useless_excludes +def main(argv=None): + parser = argparse.ArgumentParser() + parser.add_argument('filenames', nargs='*', default=[C.CONFIG_FILE]) + args = parser.parse_args(argv) + + retv = 0 + for filename in args.filenames: + retv |= check_useless_excludes(filename) + return retv + + if __name__ == '__main__': - sys.exit(check_useless_excludes()) + exit(main()) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 0afb5004e..8adcbec05 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -252,50 +252,56 @@ def _venvs(self): class MetaRepository(LocalRepository): - # Note: the hook `entry` is passed through `shlex.split()` by the command - # runner, so to prevent issues with spaces and backslashes (on Windows) it - # must be quoted here. - meta_hooks = { - 'check-useless-excludes': { - 'name': 'Check for useless excludes', - 'files': '.pre-commit-config.yaml', - 'language': 'system', - 'entry': pipes.quote(sys.executable), - 'args': ['-m', 'pre_commit.meta_hooks.check_useless_excludes'], - }, - 'check-files-matches-any': { - 'name': 'Check hooks match any files', - 'files': '.pre-commit-config.yaml', - 'language': 'system', - 'entry': pipes.quote(sys.executable), - 'args': ['-m', 'pre_commit.meta_hooks.check_files_matches_any'], - }, - } + @cached_property + def manifest_hooks(self): + # The hooks are imported here to prevent circular imports. + from pre_commit.meta_hooks import check_files_matches_any + from pre_commit.meta_hooks import check_useless_excludes + + # Note: the hook `entry` is passed through `shlex.split()` by the + # command runner, so to prevent issues with spaces and backslashes + # (on Windows) it must be quoted here. + meta_hooks = [ + { + 'id': 'check-useless-excludes', + 'name': 'Check for useless excludes', + 'files': '.pre-commit-config.yaml', + 'language': 'system', + 'entry': pipes.quote(sys.executable), + 'args': ['-m', check_useless_excludes.__name__], + }, + { + 'id': 'check-files-matches-any', + 'name': 'Check hooks match any files', + 'files': '.pre-commit-config.yaml', + 'language': 'system', + 'entry': pipes.quote(sys.executable), + 'args': ['-m', check_files_matches_any.__name__], + }, + ] + + return { + hook['id']: apply_defaults( + validate(hook, MANIFEST_HOOK_DICT), + MANIFEST_HOOK_DICT, + ) + for hook in meta_hooks + } @cached_property def hooks(self): for hook in self.repo_config['hooks']: - if hook['id'] not in self.meta_hooks: + if hook['id'] not in self.manifest_hooks: logger.error( '`{}` is not a valid meta hook. ' 'Typo? Perhaps it is introduced in a newer version? ' - 'Often `pre-commit autoupdate` fixes this.'.format( - hook['id'], - ), + 'Often `pip install --upgrade pre-commit` fixes this.' + .format(hook['id']), ) exit(1) return tuple( - ( - hook['id'], - apply_defaults( - validate( - dict(self.meta_hooks[hook['id']], **hook), - MANIFEST_HOOK_DICT, - ), - MANIFEST_HOOK_DICT, - ), - ) + (hook['id'], _hook(self.manifest_hooks[hook['id']], hook)) for hook in self.repo_config['hooks'] ) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 24771d22f..336222d6c 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -670,104 +670,6 @@ def test_meta_hook_passes( ) -def test_useless_exclude_global( - cap_out, repo_with_passing_hook, mock_out_store_directory, -): - config = OrderedDict(( - ('exclude', 'foo'), - ( - 'repos', [ - OrderedDict(( - ('repo', 'meta'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'check-useless-excludes'), - )), - ), - ), - )), - ], - ), - )) - add_config_to_repo(repo_with_passing_hook, config) - - _test_run( - cap_out, - repo_with_passing_hook, - opts={'all_files': True}, - expected_outputs=[ - b'Check for useless excludes', - b'The global exclude pattern does not match any files', - ], - expected_ret=1, - stage=False, - ) - - -def test_useless_exclude_for_hook( - cap_out, repo_with_passing_hook, mock_out_store_directory, -): - config = OrderedDict(( - ('repo', 'meta'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'check-useless-excludes'), - ('exclude', 'foo'), - )), - ), - ), - )) - add_config_to_repo(repo_with_passing_hook, config) - - _test_run( - cap_out, - repo_with_passing_hook, - opts={'all_files': True}, - expected_outputs=[ - b'Check for useless excludes', - b'The exclude pattern for check-useless-excludes ' - b'does not match any files', - ], - expected_ret=1, - stage=False, - ) - - -def test_files_match_any( - cap_out, repo_with_passing_hook, mock_out_store_directory, -): - config = OrderedDict(( - ('repo', 'meta'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'check-files-matches-any'), - )), - OrderedDict(( - ('id', 'check-useless-excludes'), - ('files', 'foo'), - )), - ), - ), - )) - add_config_to_repo(repo_with_passing_hook, config) - - _test_run( - cap_out, - repo_with_passing_hook, - opts={'all_files': True}, - expected_outputs=[ - b'Check hooks match any files', - b'The files pattern for check-useless-excludes ' - b'does not match any files', - ], - expected_ret=1, - stage=False, - ) - - @pytest.yield_fixture def modified_config_repo(repo_with_passing_hook): with modify_config(repo_with_passing_hook, commit=False) as config: diff --git a/tests/meta_hooks/__init__.py b/tests/meta_hooks/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/meta_hooks/hook_matches_any_test.py b/tests/meta_hooks/hook_matches_any_test.py new file mode 100644 index 000000000..92c6fc45d --- /dev/null +++ b/tests/meta_hooks/hook_matches_any_test.py @@ -0,0 +1,130 @@ +from collections import OrderedDict + +from pre_commit.meta_hooks import check_files_matches_any +from pre_commit.util import cwd +from testing.fixtures import add_config_to_repo +from testing.fixtures import git_dir + + +def test_hook_excludes_everything( + capsys, tempdir_factory, mock_out_store_directory, +): + config = OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'check-useless-excludes'), + ('exclude', '.pre-commit-config.yaml'), + )), + ), + ), + )) + + repo = git_dir(tempdir_factory) + add_config_to_repo(repo, config) + + with cwd(repo): + assert check_files_matches_any.main(argv=[]) == 1 + + out, _ = capsys.readouterr() + assert 'check-useless-excludes does not apply to this repository' in out + + +def test_hook_includes_nothing( + capsys, tempdir_factory, mock_out_store_directory, +): + config = OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'check-useless-excludes'), + ('files', 'foo'), + )), + ), + ), + )) + + repo = git_dir(tempdir_factory) + add_config_to_repo(repo, config) + + with cwd(repo): + assert check_files_matches_any.main(argv=[]) == 1 + + out, _ = capsys.readouterr() + assert 'check-useless-excludes does not apply to this repository' in out + + +def test_hook_types_not_matched( + capsys, tempdir_factory, mock_out_store_directory, +): + config = OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'check-useless-excludes'), + ('types', ['python']), + )), + ), + ), + )) + + repo = git_dir(tempdir_factory) + add_config_to_repo(repo, config) + + with cwd(repo): + assert check_files_matches_any.main(argv=[]) == 1 + + out, _ = capsys.readouterr() + assert 'check-useless-excludes does not apply to this repository' in out + + +def test_hook_types_excludes_everything( + capsys, tempdir_factory, mock_out_store_directory, +): + config = OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'check-useless-excludes'), + ('exclude_types', ['yaml']), + )), + ), + ), + )) + + repo = git_dir(tempdir_factory) + add_config_to_repo(repo, config) + + with cwd(repo): + assert check_files_matches_any.main(argv=[]) == 1 + + out, _ = capsys.readouterr() + assert 'check-useless-excludes does not apply to this repository' in out + + +def test_valid_includes( + capsys, tempdir_factory, mock_out_store_directory, +): + config = OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'check-useless-excludes'), + )), + ), + ), + )) + + repo = git_dir(tempdir_factory) + add_config_to_repo(repo, config) + + with cwd(repo): + assert check_files_matches_any.main(argv=[]) == 0 + + out, _ = capsys.readouterr() + assert out == '' diff --git a/tests/meta_hooks/useless_excludes_test.py b/tests/meta_hooks/useless_excludes_test.py new file mode 100644 index 000000000..8b6ba7b0f --- /dev/null +++ b/tests/meta_hooks/useless_excludes_test.py @@ -0,0 +1,107 @@ +from collections import OrderedDict + +from pre_commit.meta_hooks import check_useless_excludes +from pre_commit.util import cwd +from testing.fixtures import add_config_to_repo +from testing.fixtures import git_dir + + +def test_useless_exclude_global(capsys, tempdir_factory): + config = OrderedDict(( + ('exclude', 'foo'), + ( + 'repos', [ + OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'check-useless-excludes'), + )), + ), + ), + )), + ], + ), + )) + + repo = git_dir(tempdir_factory) + add_config_to_repo(repo, config) + + with cwd(repo): + assert check_useless_excludes.main(argv=[]) == 1 + + out, _ = capsys.readouterr() + assert "The global exclude pattern 'foo' does not match any files" in out + + +def test_useless_exclude_for_hook(capsys, tempdir_factory): + config = OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'check-useless-excludes'), + ('exclude', 'foo'), + )), + ), + ), + )) + + repo = git_dir(tempdir_factory) + add_config_to_repo(repo, config) + + with cwd(repo): + assert check_useless_excludes.main(argv=[]) == 1 + + out, _ = capsys.readouterr() + expected = ( + "The exclude pattern 'foo' for check-useless-excludes " + "does not match any files" + ) + assert expected in out + + +def test_no_excludes(capsys, tempdir_factory): + config = OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'check-useless-excludes'), + )), + ), + ), + )) + + repo = git_dir(tempdir_factory) + add_config_to_repo(repo, config) + + with cwd(repo): + assert check_useless_excludes.main(argv=[]) == 0 + + out, _ = capsys.readouterr() + assert out == '' + + +def test_valid_exclude(capsys, tempdir_factory): + config = OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'check-useless-excludes'), + ('exclude', '.pre-commit-config.yaml'), + )), + ), + ), + )) + + repo = git_dir(tempdir_factory) + add_config_to_repo(repo, config) + + with cwd(repo): + assert check_useless_excludes.main(argv=[]) == 0 + + out, _ = capsys.readouterr() + assert out == '' diff --git a/tests/repository_test.py b/tests/repository_test.py index 804894066..f7c027cd0 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -729,7 +729,7 @@ def test_meta_hook_not_present(store, fake_log_handler): assert fake_log_handler.handle.call_args[0][0].msg == ( '`i-dont-exist` is not a valid meta hook. ' 'Typo? Perhaps it is introduced in a newer version? ' - 'Often `pre-commit autoupdate` fixes this.' + 'Often `pip install --upgrade pre-commit` fixes this.' ) From 5a8ca2ffbe72fc5142aa4a0a1de52f9d3f032d71 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Oct 2017 09:12:48 -0700 Subject: [PATCH 103/544] Some minor fixups --- .pre-commit-config.yaml | 4 ++ pre_commit/commands/autoupdate.py | 3 +- .../meta_hooks/check_files_matches_any.py | 12 +++--- .../meta_hooks/check_useless_excludes.py | 27 +++++++----- pre_commit/repository.py | 43 +++++++++---------- tests/commands/autoupdate_test.py | 18 ++++++++ tests/meta_hooks/hook_matches_any_test.py | 10 ++--- tests/meta_hooks/useless_excludes_test.py | 8 ++-- 8 files changed, 77 insertions(+), 48 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 363406421..9cd637606 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,3 +25,7 @@ repos: sha: v0.6.4 hooks: - id: add-trailing-comma +- repo: meta + hooks: + - id: check-useless-excludes + - id: check-files-matches-any diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index a80c6b403..ca0ed5e25 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -11,6 +11,7 @@ from pre_commit import output from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import is_local_repo +from pre_commit.clientlib import is_meta_repo from pre_commit.clientlib import load_config from pre_commit.commands.migrate_config import migrate_config from pre_commit.repository import Repository @@ -115,7 +116,7 @@ def autoupdate(runner, tags_only): input_config = load_config(runner.config_file_path) for repo_config in input_config['repos']: - if is_local_repo(repo_config): + if is_local_repo(repo_config) or is_meta_repo(repo_config): output_repos.append(repo_config) continue output.write('Updating {}...'.format(repo_config['repo'])) diff --git a/pre_commit/meta_hooks/check_files_matches_any.py b/pre_commit/meta_hooks/check_files_matches_any.py index d253939e6..8c9a92d83 100644 --- a/pre_commit/meta_hooks/check_files_matches_any.py +++ b/pre_commit/meta_hooks/check_files_matches_any.py @@ -1,16 +1,16 @@ import argparse import pre_commit.constants as C +from pre_commit import git from pre_commit.commands.run import _filter_by_include_exclude from pre_commit.commands.run import _filter_by_types -from pre_commit.git import get_all_files from pre_commit.runner import Runner def check_all_hooks_match_files(config_file): runner = Runner.create(config_file) - files = get_all_files() - files_matched = True + files = git.get_all_files() + retv = 0 for repo in runner.repositories: for hook_id, hook in repo.hooks: @@ -20,9 +20,9 @@ def check_all_hooks_match_files(config_file): filtered = _filter_by_types(filtered, types, exclude_types) if not filtered: print('{} does not apply to this repository'.format(hook_id)) - files_matched = False + retv = 1 - return files_matched + return retv def main(argv=None): @@ -32,7 +32,7 @@ def main(argv=None): retv = 0 for filename in args.filenames: - retv |= not check_all_hooks_match_files(filename) + retv |= check_all_hooks_match_files(filename) return retv diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 89448d786..189633a8a 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -4,11 +4,15 @@ import re import pre_commit.constants as C +from pre_commit import git from pre_commit.clientlib import load_config -from pre_commit.git import get_all_files +from pre_commit.clientlib import MANIFEST_HOOK_DICT +from pre_commit.schema import apply_defaults def exclude_matches_any(filenames, include, exclude): + if exclude == '^$': + return True include_re, exclude_re = re.compile(include), re.compile(exclude) for filename in filenames: if include_re.search(filename) and exclude_re.search(filename): @@ -18,28 +22,31 @@ def exclude_matches_any(filenames, include, exclude): def check_useless_excludes(config_file): config = load_config(config_file) - files = get_all_files() - useless_excludes = False + files = git.get_all_files() + retv = 0 - exclude = config.get('exclude') - if exclude != '^$' and not exclude_matches_any(files, '', exclude): + exclude = config['exclude'] + if not exclude_matches_any(files, '', exclude): print( 'The global exclude pattern {!r} does not match any files' .format(exclude), ) - useless_excludes = True + retv = 1 for repo in config['repos']: for hook in repo['hooks']: - include, exclude = hook.get('files', ''), hook.get('exclude') - if exclude and not exclude_matches_any(files, include, exclude): + # Not actually a manifest dict, but this more accurately reflects + # the defaults applied during runtime + hook = apply_defaults(hook, MANIFEST_HOOK_DICT) + include, exclude = hook['files'], hook['exclude'] + if not exclude_matches_any(files, include, exclude): print( 'The exclude pattern {!r} for {} does not match any files' .format(exclude, hook['id']), ) - useless_excludes = True + retv = 1 - return useless_excludes + return retv def main(argv=None): diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 8adcbec05..2eb62ecb1 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -128,6 +128,12 @@ def _hook(*hook_dicts): return ret +def _hook_from_manifest_dct(dct): + dct = validate(apply_defaults(dct, MANIFEST_HOOK_DICT), MANIFEST_HOOK_DICT) + dct = _hook(dct) + return dct + + class Repository(object): def __init__(self, repo_config, store): self.repo_config = repo_config @@ -226,14 +232,8 @@ def manifest(self): @cached_property def hooks(self): - def _from_manifest_dct(dct): - dct = validate(dct, MANIFEST_HOOK_DICT) - dct = apply_defaults(dct, MANIFEST_HOOK_DICT) - dct = _hook(dct) - return dct - return tuple( - (hook['id'], _from_manifest_dct(hook)) + (hook['id'], _hook_from_manifest_dct(hook)) for hook in self.repo_config['hooks'] ) @@ -258,33 +258,32 @@ def manifest_hooks(self): from pre_commit.meta_hooks import check_files_matches_any from pre_commit.meta_hooks import check_useless_excludes - # Note: the hook `entry` is passed through `shlex.split()` by the - # command runner, so to prevent issues with spaces and backslashes - # (on Windows) it must be quoted here. + def _make_entry(mod): + """the hook `entry` is passed through `shlex.split()` by the + command runner, so to prevent issues with spaces and backslashes + (on Windows) it must be quoted here. + """ + return '{} -m {}'.format(pipes.quote(sys.executable), mod.__name__) + meta_hooks = [ { - 'id': 'check-useless-excludes', - 'name': 'Check for useless excludes', + 'id': 'check-files-matches-any', + 'name': 'Check hooks match any files', 'files': '.pre-commit-config.yaml', 'language': 'system', - 'entry': pipes.quote(sys.executable), - 'args': ['-m', check_useless_excludes.__name__], + 'entry': _make_entry(check_files_matches_any), }, { - 'id': 'check-files-matches-any', - 'name': 'Check hooks match any files', + 'id': 'check-useless-excludes', + 'name': 'Check for useless excludes', 'files': '.pre-commit-config.yaml', 'language': 'system', - 'entry': pipes.quote(sys.executable), - 'args': ['-m', check_files_matches_any.__name__], + 'entry': _make_entry(check_useless_excludes), }, ] return { - hook['id']: apply_defaults( - validate(hook, MANIFEST_HOOK_DICT), - MANIFEST_HOOK_DICT, - ) + hook['id']: _hook_from_manifest_dct(hook) for hook in meta_hooks } diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 9ae70c641..7119c6be6 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -295,6 +295,24 @@ def test_autoupdate_local_hooks_with_out_of_date_repo( assert new_config_writen['repos'][0] == local_config +def test_autoupdate_meta_hooks(tmpdir, capsys): + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write( + 'repos:\n' + '- repo: meta\n' + ' hooks:\n' + ' - id: check-useless-excludes\n', + ) + ret = autoupdate(Runner(tmpdir.strpath, C.CONFIG_FILE), tags_only=True) + assert ret == 0 + assert cfg.read() == ( + 'repos:\n' + '- repo: meta\n' + ' hooks:\n' + ' - id: check-useless-excludes\n' + ) + + def test_updates_old_format_to_new_format(tmpdir, capsys): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write( diff --git a/tests/meta_hooks/hook_matches_any_test.py b/tests/meta_hooks/hook_matches_any_test.py index 92c6fc45d..005cdf68a 100644 --- a/tests/meta_hooks/hook_matches_any_test.py +++ b/tests/meta_hooks/hook_matches_any_test.py @@ -25,7 +25,7 @@ def test_hook_excludes_everything( add_config_to_repo(repo, config) with cwd(repo): - assert check_files_matches_any.main(argv=[]) == 1 + assert check_files_matches_any.main(()) == 1 out, _ = capsys.readouterr() assert 'check-useless-excludes does not apply to this repository' in out @@ -50,7 +50,7 @@ def test_hook_includes_nothing( add_config_to_repo(repo, config) with cwd(repo): - assert check_files_matches_any.main(argv=[]) == 1 + assert check_files_matches_any.main(()) == 1 out, _ = capsys.readouterr() assert 'check-useless-excludes does not apply to this repository' in out @@ -75,7 +75,7 @@ def test_hook_types_not_matched( add_config_to_repo(repo, config) with cwd(repo): - assert check_files_matches_any.main(argv=[]) == 1 + assert check_files_matches_any.main(()) == 1 out, _ = capsys.readouterr() assert 'check-useless-excludes does not apply to this repository' in out @@ -100,7 +100,7 @@ def test_hook_types_excludes_everything( add_config_to_repo(repo, config) with cwd(repo): - assert check_files_matches_any.main(argv=[]) == 1 + assert check_files_matches_any.main(()) == 1 out, _ = capsys.readouterr() assert 'check-useless-excludes does not apply to this repository' in out @@ -124,7 +124,7 @@ def test_valid_includes( add_config_to_repo(repo, config) with cwd(repo): - assert check_files_matches_any.main(argv=[]) == 0 + assert check_files_matches_any.main(()) == 0 out, _ = capsys.readouterr() assert out == '' diff --git a/tests/meta_hooks/useless_excludes_test.py b/tests/meta_hooks/useless_excludes_test.py index 8b6ba7b0f..08b87aa8a 100644 --- a/tests/meta_hooks/useless_excludes_test.py +++ b/tests/meta_hooks/useless_excludes_test.py @@ -29,7 +29,7 @@ def test_useless_exclude_global(capsys, tempdir_factory): add_config_to_repo(repo, config) with cwd(repo): - assert check_useless_excludes.main(argv=[]) == 1 + assert check_useless_excludes.main(()) == 1 out, _ = capsys.readouterr() assert "The global exclude pattern 'foo' does not match any files" in out @@ -52,7 +52,7 @@ def test_useless_exclude_for_hook(capsys, tempdir_factory): add_config_to_repo(repo, config) with cwd(repo): - assert check_useless_excludes.main(argv=[]) == 1 + assert check_useless_excludes.main(()) == 1 out, _ = capsys.readouterr() expected = ( @@ -78,7 +78,7 @@ def test_no_excludes(capsys, tempdir_factory): add_config_to_repo(repo, config) with cwd(repo): - assert check_useless_excludes.main(argv=[]) == 0 + assert check_useless_excludes.main(()) == 0 out, _ = capsys.readouterr() assert out == '' @@ -101,7 +101,7 @@ def test_valid_exclude(capsys, tempdir_factory): add_config_to_repo(repo, config) with cwd(repo): - assert check_useless_excludes.main(argv=[]) == 0 + assert check_useless_excludes.main(()) == 0 out, _ = capsys.readouterr() assert out == '' From 2e5b4fcf4c1d816803091d2781c105cc4e44175c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 2 Nov 2017 15:30:08 -0700 Subject: [PATCH 104/544] Add comment about Z flag for selinux --- pre_commit/languages/docker.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 0d063cb97..f5eed7523 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -82,6 +82,9 @@ def docker_cmd(): 'docker', 'run', '--rm', '-u', '{}:{}'.format(os.getuid(), os.getgid()), + # https://docs.docker.com/engine/reference/commandline/run/#mount-volumes-from-container-volumes-from + # The `Z` option tells Docker to label the content with a private + # unshared label. Only the current container can use a private volume. '-v', '{}:/src:rw,Z'.format(os.getcwd()), '--workdir', '/src', ) From 56fca92a4278d6281fccaa1034b4b50f6749772a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 2 Nov 2017 15:48:43 -0700 Subject: [PATCH 105/544] Remove slowtests=false setting It wasn't actually working because of tox, I also don't use this. --- CONTRIBUTING.md | 3 --- testing/util.py | 5 ----- tests/make_archives_test.py | 2 -- tests/repository_test.py | 12 ------------ 4 files changed, 22 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ae4511f7d..7af11c424 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,9 +39,6 @@ Alternatively, with the environment activated you can run all of the tests using: `py.test tests` -To skip the slower ruby / node integration tests, you can set the environment -variable `slowtests=false`. - ### Setting up the hooks With the environment activated simply run `pre-commit install`. diff --git a/testing/util.py b/testing/util.py index 8a5bfc4d3..357968fb8 100644 --- a/testing/util.py +++ b/testing/util.py @@ -32,11 +32,6 @@ def cmd_output_mocked_pre_commit_home(*args, **kwargs): reason='Docker isn\'t running or can\'t be accessed', ) -skipif_slowtests_false = pytest.mark.skipif( - os.environ.get('slowtests') == 'false', - reason='slowtests=false', -) - skipif_cant_run_swift = pytest.mark.skipif( parse_shebang.find_executable('swift') is None, reason='swift isn\'t installed or can\'t be found', diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py index 9a0f1e591..2cb626971 100644 --- a/tests/make_archives_test.py +++ b/tests/make_archives_test.py @@ -11,7 +11,6 @@ from pre_commit.util import cmd_output from pre_commit.util import cwd from testing.fixtures import git_dir -from testing.util import skipif_slowtests_false def test_make_archive(tempdir_factory): @@ -50,7 +49,6 @@ def test_make_archive(tempdir_factory): assert not os.path.exists(os.path.join(extract_dir, 'foo', 'bar')) -@skipif_slowtests_false @pytest.mark.integration def test_main(tmpdir): make_archives.main(('--dest', tmpdir.strpath)) diff --git a/tests/repository_test.py b/tests/repository_test.py index f7c027cd0..dd20dd0e7 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -31,7 +31,6 @@ from testing.util import get_resource_path from testing.util import skipif_cant_run_docker from testing.util import skipif_cant_run_swift -from testing.util import skipif_slowtests_false from testing.util import xfailif_no_pcre_support from testing.util import xfailif_windows_no_node from testing.util import xfailif_windows_no_ruby @@ -145,7 +144,6 @@ def test_versioned_python_hook(tempdir_factory, store): ) -@skipif_slowtests_false @skipif_cant_run_docker @pytest.mark.integration def test_run_a_docker_hook(tempdir_factory, store): @@ -156,7 +154,6 @@ def test_run_a_docker_hook(tempdir_factory, store): ) -@skipif_slowtests_false @skipif_cant_run_docker @pytest.mark.integration def test_run_a_docker_hook_with_entry_args(tempdir_factory, store): @@ -167,7 +164,6 @@ def test_run_a_docker_hook_with_entry_args(tempdir_factory, store): ) -@skipif_slowtests_false @skipif_cant_run_docker @pytest.mark.integration def test_run_a_failing_docker_hook(tempdir_factory, store): @@ -179,7 +175,6 @@ def test_run_a_failing_docker_hook(tempdir_factory, store): ) -@skipif_slowtests_false @skipif_cant_run_docker @pytest.mark.integration @pytest.mark.parametrize('hook_id', ('echo-entrypoint', 'echo-cmd')) @@ -191,7 +186,6 @@ def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): ) -@skipif_slowtests_false @xfailif_windows_no_node @pytest.mark.integration def test_run_a_node_hook(tempdir_factory, store): @@ -201,7 +195,6 @@ def test_run_a_node_hook(tempdir_factory, store): ) -@skipif_slowtests_false @xfailif_windows_no_node @pytest.mark.integration def test_run_versioned_node_hook(tempdir_factory, store): @@ -211,7 +204,6 @@ def test_run_versioned_node_hook(tempdir_factory, store): ) -@skipif_slowtests_false @xfailif_windows_no_ruby @pytest.mark.integration def test_run_a_ruby_hook(tempdir_factory, store): @@ -221,7 +213,6 @@ def test_run_a_ruby_hook(tempdir_factory, store): ) -@skipif_slowtests_false @xfailif_windows_no_ruby @pytest.mark.integration def test_run_versioned_ruby_hook(tempdir_factory, store): @@ -233,7 +224,6 @@ def test_run_versioned_ruby_hook(tempdir_factory, store): ) -@skipif_slowtests_false @xfailif_windows_no_ruby @pytest.mark.integration def test_run_ruby_hook_with_disable_shared_gems( @@ -499,7 +489,6 @@ def test_additional_dependencies_roll_forward(tempdir_factory, store): assert 'mccabe' in output -@skipif_slowtests_false @xfailif_windows_no_ruby @pytest.mark.integration def test_additional_ruby_dependencies_installed( @@ -516,7 +505,6 @@ def test_additional_ruby_dependencies_installed( assert 'tins' in output -@skipif_slowtests_false @xfailif_windows_no_node @pytest.mark.integration def test_additional_node_dependencies_installed( From e99813f117dbb819539c407af80151a1b641aa82 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 3 Nov 2017 09:18:58 -0700 Subject: [PATCH 106/544] Rename check-files-matches-any to check-hooks-apply --- .pre-commit-config.yaml | 2 +- ...eck_files_matches_any.py => check_hooks_apply.py} | 0 pre_commit/repository.py | 8 ++++---- ...matches_any_test.py => check_hooks_apply_test.py} | 12 ++++++------ 4 files changed, 11 insertions(+), 11 deletions(-) rename pre_commit/meta_hooks/{check_files_matches_any.py => check_hooks_apply.py} (100%) rename tests/meta_hooks/{hook_matches_any_test.py => check_hooks_apply_test.py} (90%) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9cd637606..11f0ac29c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,5 +27,5 @@ repos: - id: add-trailing-comma - repo: meta hooks: + - id: check-hooks-apply - id: check-useless-excludes - - id: check-files-matches-any diff --git a/pre_commit/meta_hooks/check_files_matches_any.py b/pre_commit/meta_hooks/check_hooks_apply.py similarity index 100% rename from pre_commit/meta_hooks/check_files_matches_any.py rename to pre_commit/meta_hooks/check_hooks_apply.py diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 2eb62ecb1..bc0ecad30 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -255,7 +255,7 @@ class MetaRepository(LocalRepository): @cached_property def manifest_hooks(self): # The hooks are imported here to prevent circular imports. - from pre_commit.meta_hooks import check_files_matches_any + from pre_commit.meta_hooks import check_hooks_apply from pre_commit.meta_hooks import check_useless_excludes def _make_entry(mod): @@ -267,11 +267,11 @@ def _make_entry(mod): meta_hooks = [ { - 'id': 'check-files-matches-any', - 'name': 'Check hooks match any files', + 'id': 'check-hooks-apply', + 'name': 'Check hooks apply to the repository', 'files': '.pre-commit-config.yaml', 'language': 'system', - 'entry': _make_entry(check_files_matches_any), + 'entry': _make_entry(check_hooks_apply), }, { 'id': 'check-useless-excludes', diff --git a/tests/meta_hooks/hook_matches_any_test.py b/tests/meta_hooks/check_hooks_apply_test.py similarity index 90% rename from tests/meta_hooks/hook_matches_any_test.py rename to tests/meta_hooks/check_hooks_apply_test.py index 005cdf68a..0ca688025 100644 --- a/tests/meta_hooks/hook_matches_any_test.py +++ b/tests/meta_hooks/check_hooks_apply_test.py @@ -1,6 +1,6 @@ from collections import OrderedDict -from pre_commit.meta_hooks import check_files_matches_any +from pre_commit.meta_hooks import check_hooks_apply from pre_commit.util import cwd from testing.fixtures import add_config_to_repo from testing.fixtures import git_dir @@ -25,7 +25,7 @@ def test_hook_excludes_everything( add_config_to_repo(repo, config) with cwd(repo): - assert check_files_matches_any.main(()) == 1 + assert check_hooks_apply.main(()) == 1 out, _ = capsys.readouterr() assert 'check-useless-excludes does not apply to this repository' in out @@ -50,7 +50,7 @@ def test_hook_includes_nothing( add_config_to_repo(repo, config) with cwd(repo): - assert check_files_matches_any.main(()) == 1 + assert check_hooks_apply.main(()) == 1 out, _ = capsys.readouterr() assert 'check-useless-excludes does not apply to this repository' in out @@ -75,7 +75,7 @@ def test_hook_types_not_matched( add_config_to_repo(repo, config) with cwd(repo): - assert check_files_matches_any.main(()) == 1 + assert check_hooks_apply.main(()) == 1 out, _ = capsys.readouterr() assert 'check-useless-excludes does not apply to this repository' in out @@ -100,7 +100,7 @@ def test_hook_types_excludes_everything( add_config_to_repo(repo, config) with cwd(repo): - assert check_files_matches_any.main(()) == 1 + assert check_hooks_apply.main(()) == 1 out, _ = capsys.readouterr() assert 'check-useless-excludes does not apply to this repository' in out @@ -124,7 +124,7 @@ def test_valid_includes( add_config_to_repo(repo, config) with cwd(repo): - assert check_files_matches_any.main(()) == 0 + assert check_hooks_apply.main(()) == 0 out, _ = capsys.readouterr() assert out == '' From ae5b74ad38d6d9ccbe57581d7df63b72adb5c0ea Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 3 Nov 2017 09:28:57 -0700 Subject: [PATCH 107/544] always_run hooks always apply to the repository --- pre_commit/meta_hooks/check_hooks_apply.py | 2 ++ tests/meta_hooks/check_hooks_apply_test.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index 8c9a92d83..20d7f069f 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -14,6 +14,8 @@ def check_all_hooks_match_files(config_file): for repo in runner.repositories: for hook_id, hook in repo.hooks: + if hook['always_run']: + continue include, exclude = hook['files'], hook['exclude'] filtered = _filter_by_include_exclude(files, include, exclude) types, exclude_types = hook['types'], hook['exclude_types'] diff --git a/tests/meta_hooks/check_hooks_apply_test.py b/tests/meta_hooks/check_hooks_apply_test.py index 0ca688025..86bc598dd 100644 --- a/tests/meta_hooks/check_hooks_apply_test.py +++ b/tests/meta_hooks/check_hooks_apply_test.py @@ -116,6 +116,12 @@ def test_valid_includes( OrderedDict(( ('id', 'check-useless-excludes'), )), + # Should not be reported as an error due to always_run + OrderedDict(( + ('id', 'check-useless-excludes'), + ('files', '^$'), + ('always_run', True), + )), ), ), )) From 4d0c400066d2a40fb3c9022c72e3be7ffbad0887 Mon Sep 17 00:00:00 2001 From: Kevin Hock Date: Mon, 6 Nov 2017 17:13:47 -0800 Subject: [PATCH 108/544] Add repo option to autoupdate --- pre_commit/commands/autoupdate.py | 6 +++++- pre_commit/main.py | 12 +++++++++++- tests/commands/autoupdate_test.py | 23 ++++++++++++++++++++++- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index ca0ed5e25..ca8f9bb1f 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -106,7 +106,7 @@ def _write_new_config_file(path, output): f.write(to_write) -def autoupdate(runner, tags_only): +def autoupdate(runner, tags_only, repo=None): """Auto-update the pre-commit config to the latest versions of repos.""" migrate_config(runner, quiet=True) retv = 0 @@ -116,6 +116,10 @@ def autoupdate(runner, tags_only): input_config = load_config(runner.config_file_path) for repo_config in input_config['repos']: + # Skip any repo_configs that aren't the specified repo + if repo and repo != repo_config['repo']: + continue + if is_local_repo(repo_config) or is_meta_repo(repo_config): output_repos.append(repo_config) continue diff --git a/pre_commit/main.py b/pre_commit/main.py index 1405203c1..fb5dd2919 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -167,6 +167,12 @@ def main(argv=None): 'tagged version (the default behavior).' ), ) + autoupdate_parser.add_argument( + '--repo', nargs=1, default=None, + help=( + 'Repository to update the hooks of.' + ), + ) migrate_config_parser = subparsers.add_parser( 'migrate-config', @@ -245,7 +251,11 @@ def main(argv=None): elif args.command == 'autoupdate': if args.tags_only: logger.warning('--tags-only is the default') - return autoupdate(runner, tags_only=not args.bleeding_edge) + return autoupdate( + runner, + tags_only=not args.bleeding_edge, + repo=args.repo, + ) elif args.command == 'migrate-config': return migrate_config(runner) elif args.command == 'run': diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 7119c6be6..584400cd0 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -114,8 +114,11 @@ def test_autoupdate_out_of_date_repo( ) write_config('.', config) + runner = Runner('.', C.CONFIG_FILE) before = open(C.CONFIG_FILE).read() - ret = autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) + repo_name = 'file://{}'.format(out_of_date_repo.path) + # It will update the repo, because the name matches + ret = autoupdate(runner, tags_only=False, repo=repo_name) after = open(C.CONFIG_FILE).read() assert ret == 0 assert before != after @@ -124,6 +127,24 @@ def test_autoupdate_out_of_date_repo( assert out_of_date_repo.head_sha in after +def test_autoupdate_out_of_date_repo_wrong_repo_name( + out_of_date_repo, in_tmpdir, mock_out_store_directory, +): + # Write out the config + config = make_config_from_repo( + out_of_date_repo.path, sha=out_of_date_repo.original_sha, check=False, + ) + write_config('.', config) + + runner = Runner('.', C.CONFIG_FILE) + before = open(C.CONFIG_FILE).read() + # It will not update it, because the name doesn't match + ret = autoupdate(runner, tags_only=False, repo='wrong_repo_name') + after = open(C.CONFIG_FILE).read() + assert ret == 0 + assert before == after + + def test_does_not_reformat( out_of_date_repo, mock_out_store_directory, in_tmpdir, ): From e5b8cb0f7050224a3d904210f46b46a5e875e54e Mon Sep 17 00:00:00 2001 From: Kevin Hock Date: Mon, 6 Nov 2017 17:50:24 -0800 Subject: [PATCH 109/544] Keep original test as is --- tests/commands/autoupdate_test.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 584400cd0..53670ca2e 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -114,6 +114,26 @@ def test_autoupdate_out_of_date_repo( ) write_config('.', config) + runner = Runner('.', C.CONFIG_FILE) + before = open(C.CONFIG_FILE).read() + # It will update the repo, because the name matches + ret = autoupdate(runner, tags_only=False) + after = open(C.CONFIG_FILE).read() + assert ret == 0 + assert before != after + # Make sure we don't add defaults + assert 'exclude' not in after + assert out_of_date_repo.head_sha in after + +def test_autoupdate_out_of_date_repo_with_correct_repo_name( + out_of_date_repo, in_tmpdir, mock_out_store_directory, +): + # Write out the config + config = make_config_from_repo( + out_of_date_repo.path, sha=out_of_date_repo.original_sha, check=False, + ) + write_config('.', config) + runner = Runner('.', C.CONFIG_FILE) before = open(C.CONFIG_FILE).read() repo_name = 'file://{}'.format(out_of_date_repo.path) @@ -126,8 +146,7 @@ def test_autoupdate_out_of_date_repo( assert 'exclude' not in after assert out_of_date_repo.head_sha in after - -def test_autoupdate_out_of_date_repo_wrong_repo_name( +def test_autoupdate_out_of_date_repo_with_wrong_repo_name( out_of_date_repo, in_tmpdir, mock_out_store_directory, ): # Write out the config @@ -144,7 +163,6 @@ def test_autoupdate_out_of_date_repo_wrong_repo_name( assert ret == 0 assert before == after - def test_does_not_reformat( out_of_date_repo, mock_out_store_directory, in_tmpdir, ): From 70e7d9c5c4bd71b91c5e6de256a731af214e8bd1 Mon Sep 17 00:00:00 2001 From: Kevin Hock Date: Mon, 6 Nov 2017 17:55:43 -0800 Subject: [PATCH 110/544] Keep original test as is, for real --- tests/commands/autoupdate_test.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 53670ca2e..e26188801 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -114,10 +114,8 @@ def test_autoupdate_out_of_date_repo( ) write_config('.', config) - runner = Runner('.', C.CONFIG_FILE) before = open(C.CONFIG_FILE).read() - # It will update the repo, because the name matches - ret = autoupdate(runner, tags_only=False) + ret = autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) after = open(C.CONFIG_FILE).read() assert ret == 0 assert before != after @@ -137,7 +135,6 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( runner = Runner('.', C.CONFIG_FILE) before = open(C.CONFIG_FILE).read() repo_name = 'file://{}'.format(out_of_date_repo.path) - # It will update the repo, because the name matches ret = autoupdate(runner, tags_only=False, repo=repo_name) after = open(C.CONFIG_FILE).read() assert ret == 0 From fccb4e69350b8574ec34da5252b5f81da68e344b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 6 Nov 2017 18:14:59 -0800 Subject: [PATCH 111/544] Minor fixes --- pre_commit/main.py | 5 +---- tests/commands/autoupdate_test.py | 5 +++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index fb5dd2919..4c9202ad8 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -168,10 +168,7 @@ def main(argv=None): ), ) autoupdate_parser.add_argument( - '--repo', nargs=1, default=None, - help=( - 'Repository to update the hooks of.' - ), + '--repo', help='Only update this repository.', ) migrate_config_parser = subparsers.add_parser( diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index e26188801..c78af1fb3 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -123,6 +123,7 @@ def test_autoupdate_out_of_date_repo( assert 'exclude' not in after assert out_of_date_repo.head_sha in after + def test_autoupdate_out_of_date_repo_with_correct_repo_name( out_of_date_repo, in_tmpdir, mock_out_store_directory, ): @@ -139,10 +140,9 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( after = open(C.CONFIG_FILE).read() assert ret == 0 assert before != after - # Make sure we don't add defaults - assert 'exclude' not in after assert out_of_date_repo.head_sha in after + def test_autoupdate_out_of_date_repo_with_wrong_repo_name( out_of_date_repo, in_tmpdir, mock_out_store_directory, ): @@ -160,6 +160,7 @@ def test_autoupdate_out_of_date_repo_with_wrong_repo_name( assert ret == 0 assert before == after + def test_does_not_reformat( out_of_date_repo, mock_out_store_directory, in_tmpdir, ): From dc6b9eed22fa6d374c01b3e557283ff47948510f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 8 Nov 2017 12:06:24 -0800 Subject: [PATCH 112/544] v1.4.0 --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f07185c70..8005bce6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,36 @@ +1.4.0 +===== + +### Features +- Lazily install repositories. + - When running `pre-commit run `, pre-commit will only install + the necessary repositories. + - #637 issue by @webknjaz. + - #639 PR by @asottile. +- Version defaulting now applies to local hooks as well. + - This extends #556 to apply to local hooks. + - #646 PR by @asottile. +- Add new `repo: meta` hooks. + - `meta` hooks expose some linters of the pre-commit configuration itself. + - `id: check-useless-excludes`: ensures that `exclude` directives actually + apply to *any* file in the repository. + - `id: check-hooks-apply`: ensures that the configured hooks apply to + at least one file in the repository. + - pre-commit/pre-commit-hooks#63 issue by @asottile. + - #405 issue by @asottile. + - #643 PR by @hackedd. + - #653 PR by @asottile. + - #654 PR by @asottile. +- Allow a specific repository to be autoupdated instead of all repositories. + - `pre-commit autoupdate --repo ...` + - #656 issue by @KevinHock. + - #657 PR by @KevinHock. + +### Fixes +- Apply selinux labelling option to docker volumes + - #642 PR by @jimmidyson. + + 1.3.0 ===== diff --git a/setup.py b/setup.py index 0c1e83572..9c7bd0f48 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.3.0', + version='1.4.0', author='Anthony Sottile', author_email='asottile@umich.edu', From 54ccb65a09ba613bef7272d808e748126d280f8a Mon Sep 17 00:00:00 2001 From: Kevin Hock Date: Wed, 8 Nov 2017 15:49:24 -0800 Subject: [PATCH 113/544] Add existing repo_config to output_repos --- pre_commit/commands/autoupdate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index ca8f9bb1f..08c694bec 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -118,6 +118,7 @@ def autoupdate(runner, tags_only, repo=None): for repo_config in input_config['repos']: # Skip any repo_configs that aren't the specified repo if repo and repo != repo_config['repo']: + output_repos.append(repo_config) continue if is_local_repo(repo_config) or is_meta_repo(repo_config): From e4f28a2193a5b645cdffbfbc5adbe5cfdf53da01 Mon Sep 17 00:00:00 2001 From: Kevin Hock Date: Wed, 8 Nov 2017 15:53:01 -0800 Subject: [PATCH 114/544] Edit comment --- pre_commit/commands/autoupdate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 08c694bec..85ee2981c 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -116,7 +116,7 @@ def autoupdate(runner, tags_only, repo=None): input_config = load_config(runner.config_file_path) for repo_config in input_config['repos']: - # Skip any repo_configs that aren't the specified repo + # Skip updating any repo_configs that aren't the specified repo if repo and repo != repo_config['repo']: output_repos.append(repo_config) continue From dfb058f15fd24b0670ffb5c868394cb3e9c6315d Mon Sep 17 00:00:00 2001 From: Kevin Hock Date: Wed, 8 Nov 2017 15:55:02 -0800 Subject: [PATCH 115/544] Edit comment again --- pre_commit/commands/autoupdate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 85ee2981c..d05269e99 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -116,7 +116,7 @@ def autoupdate(runner, tags_only, repo=None): input_config = load_config(runner.config_file_path) for repo_config in input_config['repos']: - # Skip updating any repo_configs that aren't the specified repo + # Skip updating any repo_configs that aren't for the specified repo if repo and repo != repo_config['repo']: output_repos.append(repo_config) continue From 090030447d42c0c6994a958b18c20976ac9b5b74 Mon Sep 17 00:00:00 2001 From: Kevin Hock Date: Wed, 8 Nov 2017 17:05:22 -0800 Subject: [PATCH 116/544] Combine blocks --- pre_commit/commands/autoupdate.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index d05269e99..5ba5a8eba 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -116,12 +116,12 @@ def autoupdate(runner, tags_only, repo=None): input_config = load_config(runner.config_file_path) for repo_config in input_config['repos']: - # Skip updating any repo_configs that aren't for the specified repo - if repo and repo != repo_config['repo']: - output_repos.append(repo_config) - continue - - if is_local_repo(repo_config) or is_meta_repo(repo_config): + if ( + is_local_repo(repo_config) or + is_meta_repo(repo_config) or + # Skip updating any repo_configs that aren't for the specified repo + repo and repo != repo_config['repo'] + ): output_repos.append(repo_config) continue output.write('Updating {}...'.format(repo_config['repo'])) From ab47d08a38c67d6e974295fb58af753b4e8930ad Mon Sep 17 00:00:00 2001 From: Kevin Hock Date: Wed, 8 Nov 2017 18:04:13 -0800 Subject: [PATCH 117/544] Make regression test that ensures autoupdate foo keeps everything else --- tests/commands/autoupdate_test.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index c78af1fb3..ee20c7dd1 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -127,10 +127,12 @@ def test_autoupdate_out_of_date_repo( def test_autoupdate_out_of_date_repo_with_correct_repo_name( out_of_date_repo, in_tmpdir, mock_out_store_directory, ): - # Write out the config - config = make_config_from_repo( + stale_config = make_config_from_repo( out_of_date_repo.path, sha=out_of_date_repo.original_sha, check=False, ) + local_config = config_with_local_hooks() + config = {'repos': [stale_config, local_config]} + # Write out the config write_config('.', config) runner = Runner('.', C.CONFIG_FILE) @@ -141,6 +143,7 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( assert ret == 0 assert before != after assert out_of_date_repo.head_sha in after + assert local_config['repo'] in after def test_autoupdate_out_of_date_repo_with_wrong_repo_name( From 41d998f1c46f371c9977dcc9d31d7b42387ed74c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 9 Nov 2017 08:36:45 -0800 Subject: [PATCH 118/544] v1.4.1 --- CHANGELOG.md | 8 ++++++++ setup.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8005bce6e..e6b441613 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +1.4.1 +===== + +### Fixes +- `pre-commit autoupdate --repo ...` no longer deletes other repos. + - #660 issue by @KevinHock. + - #661 PR by @KevinHock. + 1.4.0 ===== diff --git a/setup.py b/setup.py index 9c7bd0f48..b6f2280e4 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.4.0', + version='1.4.1', author='Anthony Sottile', author_email='asottile@umich.edu', From 978fefa57a0236f81e4d6aa5fa7e02fb1fcc00fa Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 2 Jan 2018 11:47:01 -0800 Subject: [PATCH 119/544] Slower travis-ci workaround after 2017 q4 updates --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index aebcb2328..d3ec41664 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ install: pip install coveralls tox script: tox before_install: # work around https://github.com/travis-ci/travis-ci/issues/8363 - - pyenv global system 3.5 + - which python3.5 || (pyenv install 3.5.4 && pyenv global system 3.5.4) - git --version - | if [ "$LATEST_GIT" = "1" ]; then From e3cf0975f9074f221974f7f7c82389786854999d Mon Sep 17 00:00:00 2001 From: Rory Prendergast Date: Tue, 2 Jan 2018 10:51:13 -0800 Subject: [PATCH 120/544] Adds whitelist for GIT_* env vars containing only GIT_SSH --- pre_commit/util.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index 10d78d99f..a0eb37645 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -76,8 +76,11 @@ def no_git_env(): # while running pre-commit hooks in submodules. # GIT_DIR: Causes git clone to clone wrong thing # GIT_INDEX_FILE: Causes 'error invalid object ...' during commit + + # list of explicitly whitelisted variables + allowed_git_envs = ['GIT_SSH'] return { - k: v for k, v in os.environ.items() if not k.startswith('GIT_') + k: v for k, v in os.environ.items() if not k.startswith('GIT_') or k in allowed_git_envs } From 9eadfb92fd8ac17987d9e7babf0b9c3dda85b410 Mon Sep 17 00:00:00 2001 From: Rory Prendergast Date: Tue, 2 Jan 2018 12:57:18 -0800 Subject: [PATCH 121/544] reduces line length --- pre_commit/util.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index a0eb37645..81cd3064f 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -77,10 +77,9 @@ def no_git_env(): # GIT_DIR: Causes git clone to clone wrong thing # GIT_INDEX_FILE: Causes 'error invalid object ...' during commit - # list of explicitly whitelisted variables - allowed_git_envs = ['GIT_SSH'] return { - k: v for k, v in os.environ.items() if not k.startswith('GIT_') or k in allowed_git_envs + k: v for k, v in os.environ.items() + if not k.startswith('GIT_') or k in {'GIT_SSH'} } From 355196f92ed46272f236b2f1e23ca0cf0d8be8bd Mon Sep 17 00:00:00 2001 From: Rory Prendergast Date: Tue, 2 Jan 2018 12:59:09 -0800 Subject: [PATCH 122/544] backs out unnecessary blank line --- pre_commit/util.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index 81cd3064f..081adf27c 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -76,7 +76,6 @@ def no_git_env(): # while running pre-commit hooks in submodules. # GIT_DIR: Causes git clone to clone wrong thing # GIT_INDEX_FILE: Causes 'error invalid object ...' during commit - return { k: v for k, v in os.environ.items() if not k.startswith('GIT_') or k in {'GIT_SSH'} From c5030c8dca865660ff4884bfa835b02ec9cfc3e1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 2 Jan 2018 14:13:14 -0800 Subject: [PATCH 123/544] v1.4.2 --- CHANGELOG.md | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6b441613..304530c65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +1.4.2 +===== + +### Fixes +- `pre-commit` no longer clears `GIT_SSH` environment variable when cloning. + - #671 PR by rp-tanium. + 1.4.1 ===== diff --git a/setup.py b/setup.py index b6f2280e4..8c40b36b5 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.4.1', + version='1.4.2', author='Anthony Sottile', author_email='asottile@umich.edu', From a506a1cac18b04b0b9a139bec9a670197369d4f4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 2 Jan 2018 12:47:47 -0800 Subject: [PATCH 124/544] Simplify cross version tests --- .travis.yml | 2 -- CONTRIBUTING.md | 5 ++--- .../resources/arbitrary_bytes_repo/.pre-commit-hooks.yaml | 2 +- testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml | 2 +- testing/resources/python3_hooks_repo/python3_hook/main.py | 2 +- tests/repository_test.py | 6 +++--- 6 files changed, 8 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index d3ec41664..8f91d702c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,8 +16,6 @@ matrix: install: pip install coveralls tox script: tox before_install: - # work around https://github.com/travis-ci/travis-ci/issues/8363 - - which python3.5 || (pyenv install 3.5.4 && pyenv global system 3.5.4) - git --version - | if [ "$LATEST_GIT" = "1" ]; then diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7af11c424..da27dec69 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,9 +5,8 @@ - The complete test suite depends on having at least the following installed (possibly not a complete list) - git (A sufficiently newer version is required to run pre-push tests) - - python - - python3.4 (Required by a test which checks different python versions) - - python3.5 (Required by a test which checks different python versions) + - python2 (Required by a test which checks different python versions) + - python3 (Required by a test which checks different python versions) - tox (or virtualenv) - ruby + gem - docker diff --git a/testing/resources/arbitrary_bytes_repo/.pre-commit-hooks.yaml b/testing/resources/arbitrary_bytes_repo/.pre-commit-hooks.yaml index 0320f025e..2c2370092 100644 --- a/testing/resources/arbitrary_bytes_repo/.pre-commit-hooks.yaml +++ b/testing/resources/arbitrary_bytes_repo/.pre-commit-hooks.yaml @@ -2,5 +2,5 @@ name: Python 3 Hook entry: python3-hook language: python - language_version: python3.5 + language_version: python3 files: \.py$ diff --git a/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml index 0320f025e..2c2370092 100644 --- a/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml +++ b/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml @@ -2,5 +2,5 @@ name: Python 3 Hook entry: python3-hook language: python - language_version: python3.5 + language_version: python3 files: \.py$ diff --git a/testing/resources/python3_hooks_repo/python3_hook/main.py b/testing/resources/python3_hooks_repo/python3_hook/main.py index 117c7969d..04f974e64 100644 --- a/testing/resources/python3_hooks_repo/python3_hook/main.py +++ b/testing/resources/python3_hooks_repo/python3_hook/main.py @@ -4,7 +4,7 @@ def func(): - print('{}.{}'.format(*sys.version_info[:2])) + print(sys.version_info[0]) print(repr(sys.argv[1:])) print('Hello World') return 0 diff --git a/tests/repository_test.py b/tests/repository_test.py index dd20dd0e7..2b9ab6e51 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -130,8 +130,8 @@ def run_on_version(version, expected_output): assert ret[0] == 0 assert _norm_out(ret[1]) == expected_output - run_on_version('python3.4', b'3.4\n[]\nHello World\n') - run_on_version('python3.5', b'3.5\n[]\nHello World\n') + run_on_version('python2', b'2\n[]\nHello World\n') + run_on_version('python3', b'3\n[]\nHello World\n') @pytest.mark.integration @@ -140,7 +140,7 @@ def test_versioned_python_hook(tempdir_factory, store): tempdir_factory, store, 'python3_hooks_repo', 'python3-hook', [os.devnull], - b"3.5\n['" + five.to_bytes(os.devnull) + b"']\nHello World\n", + b"3\n['" + five.to_bytes(os.devnull) + b"']\nHello World\n", ) From 753979d7209bb8f0d194c9a5cd6e60319acdbb60 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 2 Jan 2018 18:47:40 -0800 Subject: [PATCH 125/544] Detect the python version based on the py launcher --- pre_commit/languages/python.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 328524090..8d891aa3e 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -9,6 +9,7 @@ from pre_commit.envcontext import Var from pre_commit.languages import helpers from pre_commit.parse_shebang import find_executable +from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.xargs import xargs @@ -40,6 +41,17 @@ def in_env(repo_cmd_runner, language_version): yield +def _find_by_py_launcher(version): # pragma: no cover (windows only) + if version.startswith('python'): + try: + return cmd_output( + 'py', '-{}'.format(version[len('python'):]), + '-c', 'import sys; print(sys.executable)', + )[1].strip() + except CalledProcessError: + pass + + def _get_default_version(): # pragma: no cover (platform dependent) def _norm(path): _, exe = os.path.split(path.lower()) @@ -66,6 +78,9 @@ def _norm(path): if find_executable(exe): return exe + if _find_by_py_launcher(exe): + return exe + # Give a best-effort try for windows if os.path.exists(r'C:\{}\python.exe'.format(exe.replace('.', ''))): return exe @@ -99,6 +114,10 @@ def norm_version(version): if version_exec and version_exec != version: return version_exec + version_exec = _find_by_py_launcher(version) + if version_exec: + return version_exec + # If it is in the form pythonx.x search in the default # place on windows if version.startswith('python'): From 04aef9e78c0556f0fdb57f330c9c3886de61734e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 2 Jan 2018 19:33:13 -0800 Subject: [PATCH 126/544] v1.4.3 --- CHANGELOG.md | 9 ++++++++- setup.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 304530c65..33f862db0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,16 @@ +1.4.3 +===== + +### Fixes +- `pre-commit` on windows can find pythons at non-hardcoded paths. + - #674 PR by @asottile + 1.4.2 ===== ### Fixes - `pre-commit` no longer clears `GIT_SSH` environment variable when cloning. - - #671 PR by rp-tanium. + - #671 PR by @rp-tanium. 1.4.1 ===== diff --git a/setup.py b/setup.py index 8c40b36b5..ae09ecbc1 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.4.2', + version='1.4.3', author='Anthony Sottile', author_email='asottile@umich.edu', From 029ccc47c854e21d32344f7b47865013ae55270f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 7 Jan 2018 20:56:17 -0800 Subject: [PATCH 127/544] Invoke `git diff` without a pager --- pre_commit/commands/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 4cbc99bbe..3a08c8d89 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -193,7 +193,7 @@ def _run_hooks(config, repo_hooks, args, environ): subprocess.call(('git', 'diff', '--quiet', '--no-ext-diff')) != 0 ): print('All changes made by hooks:') - subprocess.call(('git', 'diff', '--no-ext-diff')) + subprocess.call(('git', '--no-pager', 'diff', '--no-ext-diff')) return retval From a5f3cefb641fd868029710c80e398fa7ae6ce545 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 7 Jan 2018 21:26:35 -0800 Subject: [PATCH 128/544] v1.4.4 --- CHANGELOG.md | 9 ++++++++- setup.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33f862db0..7caa5f1aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,16 @@ +1.4.4 +===== + +### Fixes +- Invoke `git diff` without a pager during `--show-diff-on-failure`. + - #676 PR by @asottile. + 1.4.3 ===== ### Fixes - `pre-commit` on windows can find pythons at non-hardcoded paths. - - #674 PR by @asottile + - #674 PR by @asottile. 1.4.2 ===== diff --git a/setup.py b/setup.py index ae09ecbc1..e2326b73f 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.4.3', + version='1.4.4', author='Anthony Sottile', author_email='asottile@umich.edu', From 8407b92b18f92b7c012d913e4de760d41efe2c31 Mon Sep 17 00:00:00 2001 From: Iulian Onofrei Date: Tue, 9 Jan 2018 17:51:41 +0200 Subject: [PATCH 129/544] Replace string literals with constants --- pre_commit/commands/run.py | 9 +++++---- pre_commit/main.py | 2 +- pre_commit/repository.py | 4 ++-- testing/fixtures.py | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 3a08c8d89..71fb43e7d 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -9,6 +9,7 @@ from identify.identify import tags_from_path +import pre_commit.constants as C from pre_commit import color from pre_commit import git from pre_commit import output @@ -222,10 +223,10 @@ def run(runner, args, environ=os.environ): logger.error('Specify both --origin and --source.') return 1 if _has_unstaged_config(runner) and not no_stash: - logger.error( - 'Your .pre-commit-config.yaml is unstaged.\n' - '`git add .pre-commit-config.yaml` to fix this.', - ) + logger.error(( + 'Your {0} is unstaged.\n' + '`git add {0}` to fix this.' + ).format(C.CONFIG_FILE),) return 1 # Expose origin / source as environment variables for hooks to consume diff --git a/pre_commit/main.py b/pre_commit/main.py index 4c9202ad8..865571a53 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -42,7 +42,7 @@ def _add_color_option(parser): def _add_config_option(parser): parser.add_argument( - '-c', '--config', default='.pre-commit-config.yaml', + '-c', '--config', default=C.CONFIG_FILE, help='Path to alternate config file', ) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index bc0ecad30..5c11921c4 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -269,14 +269,14 @@ def _make_entry(mod): { 'id': 'check-hooks-apply', 'name': 'Check hooks apply to the repository', - 'files': '.pre-commit-config.yaml', + 'files': C.CONFIG_FILE, 'language': 'system', 'entry': _make_entry(check_hooks_apply), }, { 'id': 'check-useless-excludes', 'name': 'Check for useless excludes', - 'files': '.pre-commit-config.yaml', + 'files': C.CONFIG_FILE, 'language': 'system', 'entry': _make_entry(check_useless_excludes), }, diff --git a/testing/fixtures.py b/testing/fixtures.py index b1c7a89f0..edb1bcdf4 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -47,7 +47,7 @@ def modify_manifest(path): with io.open(manifest_path, 'w') as manifest_file: manifest_file.write(ordered_dump(manifest, **C.YAML_DUMP_KWARGS)) cmd_output( - 'git', 'commit', '-am', 'update .pre-commit-hooks.yaml', cwd=path, + 'git', 'commit', '-am', 'update {}'.format(C.MANIFEST_FILE), cwd=path, ) From 81df782c2067b5eeb1bbf6dce991b6bb30f7e4e4 Mon Sep 17 00:00:00 2001 From: Iulian Onofrei <6d0847b9@opayq.com> Date: Tue, 9 Jan 2018 18:10:05 +0200 Subject: [PATCH 130/544] Update unstaged config file error message --- pre_commit/commands/run.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 71fb43e7d..4df2e30ea 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -223,10 +223,10 @@ def run(runner, args, environ=os.environ): logger.error('Specify both --origin and --source.') return 1 if _has_unstaged_config(runner) and not no_stash: - logger.error(( - 'Your {0} is unstaged.\n' - '`git add {0}` to fix this.' - ).format(C.CONFIG_FILE),) + logger.error( + 'Your pre-commit configuration is unstaged.\n' + '`git add {}` to fix this.'.format(runner.config_file), + ) return 1 # Expose origin / source as environment variables for hooks to consume From 2255d8484e57d8067769a8de2e581af1371140c4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 9 Jan 2018 08:15:27 -0800 Subject: [PATCH 131/544] Update message for unstaged config in test --- tests/commands/run_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 336222d6c..97c82c251 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -683,7 +683,7 @@ def test_error_with_unstaged_config( ): args = run_opts() ret, printed = _do_run(cap_out, modified_config_repo, args) - assert b'Your .pre-commit-config.yaml is unstaged.' in printed + assert b'Your pre-commit configuration is unstaged.' in printed assert ret == 1 @@ -695,7 +695,7 @@ def test_no_unstaged_error_with_all_files_or_files( ): args = run_opts(**opts) ret, printed = _do_run(cap_out, modified_config_repo, args) - assert b'Your .pre-commit-config.yaml is unstaged.' not in printed + assert b'Your pre-commit configuration is unstaged.' not in printed def test_files_running_subdir( From df38e1010bc587ed3a046131f6c3b91fa0d6fed3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 9 Jan 2018 08:49:42 -0800 Subject: [PATCH 132/544] Remove unused import --- pre_commit/commands/run.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 4df2e30ea..c70eff011 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -9,7 +9,6 @@ from identify.identify import tags_from_path -import pre_commit.constants as C from pre_commit import color from pre_commit import git from pre_commit import output From 40690064f7d350d3e4ac8047910d39d6391af0ff Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 9 Jan 2018 09:42:44 -0800 Subject: [PATCH 133/544] Fix broken local golang repos --- pre_commit/store.py | 15 +++++++++++++++ tests/repository_test.py | 18 ++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/pre_commit/store.py b/pre_commit/store.py index 3262bda2b..9c6734526 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -156,6 +156,21 @@ def clone_strategy(directory): def make_local(self, deps): def make_local_strategy(directory): copy_tree_to_path(resource_filename('empty_template'), directory) + + env = no_git_env() + name, email = 'pre-commit', 'asottile+pre-commit@umich.edu' + env['GIT_AUTHOR_NAME'] = env['GIT_COMMITTER_NAME'] = name + env['GIT_AUTHOR_EMAIL'] = env['GIT_COMMITTER_EMAIL'] = email + + # initialize the git repository so it looks more like cloned repos + def _git_cmd(*args): + cmd_output('git', '-C', directory, *args, env=env) + + _git_cmd('init', '.') + _git_cmd('config', 'remote.origin.url', '<>') + _git_cmd('add', '.') + _git_cmd('commit', '--no-edit', '--no-gpg-sign', '-n', '-minit') + return self._new_repo( 'local:{}'.format(','.join(sorted(deps))), C.LOCAL_REPO_VERSION, make_local_strategy, diff --git a/tests/repository_test.py b/tests/repository_test.py index 2b9ab6e51..1d38d2466 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -541,6 +541,24 @@ def test_additional_golang_dependencies_installed( assert 'hello' in binaries +def test_local_golang_additional_dependencies(store): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'hello', + 'name': 'hello', + 'entry': 'hello', + 'language': 'golang', + 'additional_dependencies': ['github.com/golang/example/hello'], + }], + } + repo = Repository.create(config, store) + (_, hook), = repo.hooks + ret = repo.run_hook(hook, ('filename',)) + assert ret[0] == 0 + assert _norm_out(ret[1]) == b"Hello, Go examples!\n" + + def test_reinstall(tempdir_factory, store, log_info_mock): path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) From c751f629a61c4cae1209cd8b2d2cdc8a7b5e0ee5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 9 Jan 2018 10:34:30 -0800 Subject: [PATCH 134/544] v1.4.5 --- CHANGELOG.md | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7caa5f1aa..0b1ae8499 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +1.4.5 +===== + +### Fixes +- Fix `local` golang repositories with `additional_dependencies`. + - #679 #680 issue and PR by @asottile. + +### Misc +- Replace some string literals with constants + - #678 PR by @revolter. + 1.4.4 ===== diff --git a/setup.py b/setup.py index e2326b73f..2eb42418e 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.4.4', + version='1.4.5', author='Anthony Sottile', author_email='asottile@umich.edu', From 7d87da8acdd3b8817d941b9a172be6282722bf5e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 11 Jan 2018 21:41:48 -0800 Subject: [PATCH 135/544] Move PrefixedCommandRunner -> Prefix --- pre_commit/languages/all.py | 14 ++- pre_commit/languages/docker.py | 28 +++--- pre_commit/languages/docker_image.py | 2 +- pre_commit/languages/golang.py | 22 ++--- pre_commit/languages/helpers.py | 8 +- pre_commit/languages/node.py | 27 +++--- pre_commit/languages/pcre.py | 2 +- pre_commit/languages/pygrep.py | 2 +- pre_commit/languages/python.py | 29 +++--- pre_commit/languages/ruby.py | 51 +++++----- pre_commit/languages/script.py | 4 +- pre_commit/languages/swift.py | 19 ++-- pre_commit/languages/system.py | 2 +- pre_commit/prefix.py | 20 ++++ pre_commit/prefixed_command_runner.py | 50 ---------- pre_commit/repository.py | 62 ++++++------ tests/conftest.py | 6 -- tests/languages/all_test.py | 6 +- tests/languages/ruby_test.py | 39 ++++---- tests/prefix_test.py | 57 +++++++++++ tests/prefixed_command_runner_test.py | 133 -------------------------- tests/repository_test.py | 30 +++--- tests/util_test.py | 29 ++++++ 23 files changed, 270 insertions(+), 372 deletions(-) create mode 100644 pre_commit/prefix.py delete mode 100644 pre_commit/prefixed_command_runner.py create mode 100644 tests/prefix_test.py delete mode 100644 tests/prefixed_command_runner_test.py diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 514ba6110..a56f7e79e 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -23,25 +23,25 @@ # return 'default' if there is no better option. # """ # -# def healthy(repo_cmd_runner, language_version): +# def healthy(prefix, language_version): # """Return whether or not the environment is considered functional.""" # -# def install_environment(repo_cmd_runner, version, additional_dependencies): +# def install_environment(prefix, version, additional_dependencies): # """Installs a repository in the given repository. Note that the current # working directory will already be inside the repository. # # Args: -# repo_cmd_runner - `PrefixedCommandRunner` bound to the repository. +# prefix - `Prefix` bound to the repository. # version - A version specified in the hook configuration or # 'default'. # """ # -# def run_hook(repo_cmd_runner, hook, file_args): +# def run_hook(prefix, hook, file_args): # """Runs a hook and returns the returncode and output of running that # hook. # # Args: -# repo_cmd_runner - `PrefixedCommandRunner` bound to the repository. +# prefix - `Prefix` bound to the repository. # hook - Hook dictionary # file_args - The files to be run # @@ -62,6 +62,4 @@ 'swift': swift, 'system': system, } - - -all_languages = languages.keys() +all_languages = sorted(languages) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index f5eed7523..f3c46a33d 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -22,10 +22,9 @@ def md5(s): # pragma: windows no cover return hashlib.md5(five.to_bytes(s)).hexdigest() -def docker_tag(repo_cmd_runner): # pragma: windows no cover - return 'pre-commit-{}'.format( - md5(os.path.basename(repo_cmd_runner.path())), - ).lower() +def docker_tag(prefix): # pragma: windows no cover + md5sum = md5(os.path.basename(prefix.prefix_dir)).lower() + return 'pre-commit-{}'.format(md5sum) def docker_is_running(): # pragma: windows no cover @@ -41,39 +40,36 @@ def assert_docker_available(): # pragma: windows no cover ) -def build_docker_image(repo_cmd_runner, **kwargs): # pragma: windows no cover +def build_docker_image(prefix, **kwargs): # pragma: windows no cover pull = kwargs.pop('pull') assert not kwargs, kwargs cmd = ( 'docker', 'build', - '--tag', docker_tag(repo_cmd_runner), + '--tag', docker_tag(prefix), '--label', PRE_COMMIT_LABEL, ) if pull: cmd += ('--pull',) # This must come last for old versions of docker. See #477 cmd += ('.',) - helpers.run_setup_cmd(repo_cmd_runner, cmd) + helpers.run_setup_cmd(prefix, cmd) def install_environment( - repo_cmd_runner, version, additional_dependencies, + prefix, version, additional_dependencies, ): # pragma: windows no cover - assert repo_cmd_runner.exists('Dockerfile'), ( - 'No Dockerfile was found in the hook repository' - ) helpers.assert_version_default('docker', version) helpers.assert_no_additional_deps('docker', additional_dependencies) assert_docker_available() - directory = repo_cmd_runner.path( + directory = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, 'default'), ) # Docker doesn't really have relevant disk environment, but pre-commit # still needs to cleanup it's state files on failure with clean_path_on_failure(directory): - build_docker_image(repo_cmd_runner, pull=True) + build_docker_image(prefix, pull=True) os.mkdir(directory) @@ -90,15 +86,15 @@ def docker_cmd(): ) -def run_hook(repo_cmd_runner, hook, file_args): # pragma: windows no cover +def run_hook(prefix, hook, file_args): # pragma: windows no cover assert_docker_available() # Rebuild the docker image in case it has gone missing, as many people do # automated cleanup of docker images. - build_docker_image(repo_cmd_runner, pull=False) + build_docker_image(prefix, pull=False) hook_cmd = helpers.to_cmd(hook) entry_exe, cmd_rest = hook_cmd[0], hook_cmd[1:] - entry_tag = ('--entrypoint', entry_exe, docker_tag(repo_cmd_runner)) + entry_tag = ('--entrypoint', entry_exe, docker_tag(prefix)) cmd = docker_cmd() + entry_tag + cmd_rest return xargs(cmd, file_args) diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index a6f89e3f4..6301970c4 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -13,7 +13,7 @@ install_environment = helpers.no_install -def run_hook(repo_cmd_runner, hook, file_args): # pragma: windows no cover +def run_hook(prefix, hook, file_args): # pragma: windows no cover assert_docker_available() cmd = docker_cmd() + helpers.to_cmd(hook) return xargs(cmd, file_args) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index cad7dfc6d..35cfa2ad8 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -26,8 +26,8 @@ def get_env_patch(venv): @contextlib.contextmanager -def in_env(repo_cmd_runner): - envdir = repo_cmd_runner.path( +def in_env(prefix): + envdir = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, 'default'), ) with envcontext(get_env_patch(envdir)): @@ -50,20 +50,18 @@ def guess_go_dir(remote_url): return 'unknown_src_dir' -def install_environment(repo_cmd_runner, version, additional_dependencies): +def install_environment(prefix, version, additional_dependencies): helpers.assert_version_default('golang', version) - directory = repo_cmd_runner.path( + directory = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, 'default'), ) with clean_path_on_failure(directory): - remote = git.get_remote_url(repo_cmd_runner.path()) + remote = git.get_remote_url(prefix.prefix_dir) repo_src_dir = os.path.join(directory, 'src', guess_go_dir(remote)) # Clone into the goenv we'll create - helpers.run_setup_cmd( - repo_cmd_runner, ('git', 'clone', '.', repo_src_dir), - ) + helpers.run_setup_cmd(prefix, ('git', 'clone', '.', repo_src_dir)) if sys.platform == 'cygwin': # pragma: no cover _, gopath, _ = cmd_output('cygpath', '-w', directory) @@ -75,10 +73,10 @@ def install_environment(repo_cmd_runner, version, additional_dependencies): for dependency in additional_dependencies: cmd_output('go', 'get', dependency, cwd=repo_src_dir, env=env) # Same some disk space, we don't need these after installation - rmtree(repo_cmd_runner.path(directory, 'src')) - rmtree(repo_cmd_runner.path(directory, 'pkg')) + rmtree(prefix.path(directory, 'src')) + rmtree(prefix.path(directory, 'pkg')) -def run_hook(repo_cmd_runner, hook, file_args): - with in_env(repo_cmd_runner): +def run_hook(prefix, hook, file_args): + with in_env(prefix): return xargs(helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 30082d6b4..ddbe2e80e 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -5,8 +5,8 @@ from pre_commit.util import cmd_output -def run_setup_cmd(runner, cmd): - cmd_output(*cmd, cwd=runner.prefix_dir, encoding=None) +def run_setup_cmd(prefix, cmd): + cmd_output(*cmd, cwd=prefix.prefix_dir, encoding=None) def environment_dir(ENVIRONMENT_DIR, language_version): @@ -39,9 +39,9 @@ def basic_get_default_version(): return 'default' -def basic_healthy(repo_cmd_runner, language_version): +def basic_healthy(prefix, language_version): return True -def no_install(repo_cmd_runner, version, additional_dependencies): +def no_install(prefix, version, additional_dependencies): raise AssertionError('This type is not installable') diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index aca3c4100..5f5d9fdd5 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -33,8 +33,8 @@ def get_env_patch(venv): # pragma: windows no cover @contextlib.contextmanager -def in_env(repo_cmd_runner, language_version): # pragma: windows no cover - envdir = repo_cmd_runner.path( +def in_env(prefix, language_version): # pragma: windows no cover + envdir = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, language_version), ) with envcontext(get_env_patch(envdir)): @@ -42,31 +42,26 @@ def in_env(repo_cmd_runner, language_version): # pragma: windows no cover def install_environment( - repo_cmd_runner, version, additional_dependencies, + prefix, version, additional_dependencies, ): # pragma: windows no cover additional_dependencies = tuple(additional_dependencies) - assert repo_cmd_runner.exists('package.json') + assert prefix.exists('package.json') directory = helpers.environment_dir(ENVIRONMENT_DIR, version) - env_dir = repo_cmd_runner.path(directory) + env_dir = prefix.path(directory) with clean_path_on_failure(env_dir): - cmd = [ - sys.executable, '-m', 'nodeenv', '--prebuilt', - '{{prefix}}{}'.format(directory), - ] - + cmd = [sys.executable, '-m', 'nodeenv', '--prebuilt', env_dir] if version != 'default': cmd.extend(['-n', version]) + cmd_output(*cmd) - repo_cmd_runner.run(cmd) - - with in_env(repo_cmd_runner, version): + with in_env(prefix, version): helpers.run_setup_cmd( - repo_cmd_runner, + prefix, ('npm', 'install', '-g', '.') + additional_dependencies, ) -def run_hook(repo_cmd_runner, hook, file_args): # pragma: windows no cover - with in_env(repo_cmd_runner, hook['language_version']): +def run_hook(prefix, hook, file_args): # pragma: windows no cover + with in_env(prefix, hook['language_version']): return xargs(helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py index eaacc1101..fb078ab78 100644 --- a/pre_commit/languages/pcre.py +++ b/pre_commit/languages/pcre.py @@ -13,7 +13,7 @@ install_environment = helpers.no_install -def run_hook(repo_cmd_runner, hook, file_args): +def run_hook(prefix, hook, file_args): # For PCRE the entry is the regular expression to match cmd = (GREP, '-H', '-n', '-P') + tuple(hook['args']) + (hook['entry'],) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 4914fd66d..878f57d06 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -27,7 +27,7 @@ def _process_filename_by_line(pattern, filename): return retv -def run_hook(repo_cmd_runner, hook, file_args): +def run_hook(prefix, hook, file_args): exe = (sys.executable, '-m', __name__) exe += tuple(hook['args']) + (hook['entry'],) return xargs(exe, file_args) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 8d891aa3e..7fc5443ef 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -33,8 +33,8 @@ def get_env_patch(venv): @contextlib.contextmanager -def in_env(repo_cmd_runner, language_version): - envdir = repo_cmd_runner.path( +def in_env(prefix, language_version): + envdir = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, language_version), ) with envcontext(get_env_patch(envdir)): @@ -98,8 +98,8 @@ def get_default_version(): return get_default_version() -def healthy(repo_cmd_runner, language_version): - with in_env(repo_cmd_runner, language_version): +def healthy(prefix, language_version): + with in_env(prefix, language_version): retcode, _, _ = cmd_output( 'python', '-c', 'import ctypes, datetime, io, os, ssl, weakref', retcode=None, @@ -127,29 +127,26 @@ def norm_version(version): return os.path.expanduser(version) -def install_environment(repo_cmd_runner, version, additional_dependencies): +def install_environment(prefix, version, additional_dependencies): additional_dependencies = tuple(additional_dependencies) directory = helpers.environment_dir(ENVIRONMENT_DIR, version) # Install a virtualenv - with clean_path_on_failure(repo_cmd_runner.path(directory)): - venv_cmd = [ - sys.executable, '-m', 'virtualenv', - '{{prefix}}{}'.format(directory), - ] + env_dir = prefix.path(directory) + with clean_path_on_failure(env_dir): + venv_cmd = [sys.executable, '-m', 'virtualenv', env_dir] if version != 'default': venv_cmd.extend(['-p', norm_version(version)]) else: venv_cmd.extend(['-p', os.path.realpath(sys.executable)]) venv_env = dict(os.environ, VIRTUALENV_NO_DOWNLOAD='1') - repo_cmd_runner.run(venv_cmd, cwd='/', env=venv_env) - with in_env(repo_cmd_runner, version): + cmd_output(*venv_cmd, cwd='/', env=venv_env) + with in_env(prefix, version): helpers.run_setup_cmd( - repo_cmd_runner, - ('pip', 'install', '.') + additional_dependencies, + prefix, ('pip', 'install', '.') + additional_dependencies, ) -def run_hook(repo_cmd_runner, hook, file_args): - with in_env(repo_cmd_runner, hook['language_version']): +def run_hook(prefix, hook, file_args): + with in_env(prefix, hook['language_version']): return xargs(helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index e7e0c3286..3bd7130d1 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -39,36 +39,32 @@ def get_env_patch(venv, language_version): # pragma: windows no cover @contextlib.contextmanager -def in_env(repo_cmd_runner, language_version): # pragma: windows no cover - envdir = repo_cmd_runner.path( +def in_env(prefix, language_version): # pragma: windows no cover + envdir = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, language_version), ) with envcontext(get_env_patch(envdir, language_version)): yield -def _install_rbenv( - repo_cmd_runner, version='default', -): # pragma: windows no cover +def _install_rbenv(prefix, version='default'): # pragma: windows no cover directory = helpers.environment_dir(ENVIRONMENT_DIR, version) with tarfile.open(resource_filename('rbenv.tar.gz')) as tf: - tf.extractall(repo_cmd_runner.path('.')) - shutil.move( - repo_cmd_runner.path('rbenv'), repo_cmd_runner.path(directory), - ) + tf.extractall(prefix.path('.')) + shutil.move(prefix.path('rbenv'), prefix.path(directory)) # Only install ruby-build if the version is specified if version != 'default': # ruby-download with tarfile.open(resource_filename('ruby-download.tar.gz')) as tf: - tf.extractall(repo_cmd_runner.path(directory, 'plugins')) + tf.extractall(prefix.path(directory, 'plugins')) # ruby-build with tarfile.open(resource_filename('ruby-build.tar.gz')) as tf: - tf.extractall(repo_cmd_runner.path(directory, 'plugins')) + tf.extractall(prefix.path(directory, 'plugins')) - activate_path = repo_cmd_runner.path(directory, 'bin', 'activate') + activate_path = prefix.path(directory, 'bin', 'activate') with io.open(activate_path, 'w') as activate_file: # This is similar to how you would install rbenv to your home directory # However we do a couple things to make the executables exposed and @@ -84,7 +80,7 @@ def _install_rbenv( # directory "export GEM_HOME='{directory}/gems'\n" 'export PATH="$GEM_HOME/bin:$PATH"\n' - '\n'.format(directory=repo_cmd_runner.path(directory)), + '\n'.format(directory=prefix.path(directory)), ) # If we aren't using the system ruby, add a version here @@ -101,35 +97,32 @@ def _install_ruby(runner, version): # pragma: windows no cover def install_environment( - repo_cmd_runner, version, additional_dependencies, + prefix, version, additional_dependencies, ): # pragma: windows no cover additional_dependencies = tuple(additional_dependencies) directory = helpers.environment_dir(ENVIRONMENT_DIR, version) - with clean_path_on_failure(repo_cmd_runner.path(directory)): + with clean_path_on_failure(prefix.path(directory)): # TODO: this currently will fail if there's no version specified and # there's no system ruby installed. Is this ok? - _install_rbenv(repo_cmd_runner, version=version) - with in_env(repo_cmd_runner, version): + _install_rbenv(prefix, version=version) + with in_env(prefix, version): # Need to call this before installing so rbenv's directories are # set up - helpers.run_setup_cmd(repo_cmd_runner, ('rbenv', 'init', '-')) + helpers.run_setup_cmd(prefix, ('rbenv', 'init', '-')) if version != 'default': - _install_ruby(repo_cmd_runner, version) + _install_ruby(prefix, version) # Need to call this after installing to set up the shims - helpers.run_setup_cmd(repo_cmd_runner, ('rbenv', 'rehash')) + helpers.run_setup_cmd(prefix, ('rbenv', 'rehash')) helpers.run_setup_cmd( - repo_cmd_runner, - ('gem', 'build') + repo_cmd_runner.star('.gemspec'), + prefix, ('gem', 'build') + prefix.star('.gemspec'), ) helpers.run_setup_cmd( - repo_cmd_runner, - ( - ('gem', 'install', '--no-ri', '--no-rdoc') + - repo_cmd_runner.star('.gem') + additional_dependencies - ), + prefix, + ('gem', 'install', '--no-ri', '--no-rdoc') + + prefix.star('.gem') + additional_dependencies, ) -def run_hook(repo_cmd_runner, hook, file_args): # pragma: windows no cover - with in_env(repo_cmd_runner, hook['language_version']): +def run_hook(prefix, hook, file_args): # pragma: windows no cover + with in_env(prefix, hook['language_version']): return xargs(helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 8c3b0c56b..8186e77ab 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -10,7 +10,7 @@ install_environment = helpers.no_install -def run_hook(repo_cmd_runner, hook, file_args): +def run_hook(prefix, hook, file_args): cmd = helpers.to_cmd(hook) - cmd = (repo_cmd_runner.prefix_dir + cmd[0],) + cmd[1:] + cmd = (prefix.prefix_dir + cmd[0],) + cmd[1:] return xargs(cmd, file_args) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index f4d1eb5a1..2863fbee7 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -7,6 +7,7 @@ from pre_commit.envcontext import Var from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure +from pre_commit.util import cmd_output from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'swift_env' @@ -22,8 +23,8 @@ def get_env_patch(venv): # pragma: windows no cover @contextlib.contextmanager -def in_env(repo_cmd_runner): # pragma: windows no cover - envdir = repo_cmd_runner.path( +def in_env(prefix): # pragma: windows no cover + envdir = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, 'default'), ) with envcontext(get_env_patch(envdir)): @@ -31,25 +32,25 @@ def in_env(repo_cmd_runner): # pragma: windows no cover def install_environment( - repo_cmd_runner, version, additional_dependencies, + prefix, version, additional_dependencies, ): # pragma: windows no cover helpers.assert_version_default('swift', version) helpers.assert_no_additional_deps('swift', additional_dependencies) - directory = repo_cmd_runner.path( + directory = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, 'default'), ) # Build the swift package with clean_path_on_failure(directory): os.mkdir(directory) - repo_cmd_runner.run(( + cmd_output( 'swift', 'build', - '-C', '{prefix}', + '-C', prefix.prefix_dir, '-c', BUILD_CONFIG, '--build-path', os.path.join(directory, BUILD_DIR), - )) + ) -def run_hook(repo_cmd_runner, hook, file_args): # pragma: windows no cover - with in_env(repo_cmd_runner): +def run_hook(prefix, hook, file_args): # pragma: windows no cover + with in_env(prefix): return xargs(helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 693a1601e..84cd1fe4e 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -10,5 +10,5 @@ install_environment = helpers.no_install -def run_hook(repo_cmd_runner, hook, file_args): +def run_hook(prefix, hook, file_args): return xargs(helpers.to_cmd(hook), file_args) diff --git a/pre_commit/prefix.py b/pre_commit/prefix.py new file mode 100644 index 000000000..128bd861b --- /dev/null +++ b/pre_commit/prefix.py @@ -0,0 +1,20 @@ +from __future__ import unicode_literals + +import os.path + + +class Prefix(object): + def __init__(self, prefix_dir): + self.prefix_dir = prefix_dir.rstrip(os.sep) + os.sep + + def path(self, *parts): + path = os.path.join(self.prefix_dir, *parts) + return os.path.normpath(path) + + def exists(self, *parts): + return os.path.exists(self.path(*parts)) + + def star(self, end): + return tuple( + path for path in os.listdir(self.prefix_dir) if path.endswith(end) + ) diff --git a/pre_commit/prefixed_command_runner.py b/pre_commit/prefixed_command_runner.py deleted file mode 100644 index c2de526b2..000000000 --- a/pre_commit/prefixed_command_runner.py +++ /dev/null @@ -1,50 +0,0 @@ -from __future__ import unicode_literals - -import os.path -import subprocess - -from pre_commit.util import cmd_output - - -class PrefixedCommandRunner(object): - """A PrefixedCommandRunner allows you to run subprocess commands with - comand substitution. - - For instance: - PrefixedCommandRunner('/tmp/foo').run(['{prefix}foo.sh', 'bar', 'baz']) - - will run ['/tmp/foo/foo.sh', 'bar', 'baz'] - """ - - def __init__( - self, - prefix_dir, - popen=subprocess.Popen, - makedirs=os.makedirs, - ): - self.prefix_dir = prefix_dir.rstrip(os.sep) + os.sep - self.__popen = popen - self.__makedirs = makedirs - - def _create_path_if_not_exists(self): - if not os.path.exists(self.prefix_dir): - self.__makedirs(self.prefix_dir) - - def run(self, cmd, **kwargs): - self._create_path_if_not_exists() - replaced_cmd = [ - part.replace('{prefix}', self.prefix_dir) for part in cmd - ] - return cmd_output(*replaced_cmd, __popen=self.__popen, **kwargs) - - def path(self, *parts): - path = os.path.join(self.prefix_dir, *parts) - return os.path.normpath(path) - - def exists(self, *parts): - return os.path.exists(self.path(*parts)) - - def star(self, end): - return tuple( - path for path in os.listdir(self.prefix_dir) if path.endswith(end) - ) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 5c11921c4..e01b3d1d5 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -21,7 +21,7 @@ from pre_commit.clientlib import MANIFEST_HOOK_DICT from pre_commit.languages.all import languages from pre_commit.languages.helpers import environment_dir -from pre_commit.prefixed_command_runner import PrefixedCommandRunner +from pre_commit.prefix import Prefix from pre_commit.schema import apply_defaults from pre_commit.schema import validate @@ -33,22 +33,22 @@ def _state(additional_deps): return {'additional_dependencies': sorted(additional_deps)} -def _state_filename(cmd_runner, venv): - return cmd_runner.path( +def _state_filename(prefix, venv): + return prefix.path( venv, '.install_state_v' + C.INSTALLED_STATE_VERSION, ) -def _read_state(cmd_runner, venv): - filename = _state_filename(cmd_runner, venv) +def _read_state(prefix, venv): + filename = _state_filename(prefix, venv) if not os.path.exists(filename): return None else: return json.loads(io.open(filename).read()) -def _write_state(cmd_runner, venv, state): - state_filename = _state_filename(cmd_runner, venv) +def _write_state(prefix, venv, state): + state_filename = _state_filename(prefix, venv) staging = state_filename + 'staging' with io.open(staging, 'w') as state_file: state_file.write(five.to_text(json.dumps(state))) @@ -56,24 +56,24 @@ def _write_state(cmd_runner, venv, state): os.rename(staging, state_filename) -def _installed(cmd_runner, language_name, language_version, additional_deps): +def _installed(prefix, language_name, language_version, additional_deps): language = languages[language_name] venv = environment_dir(language.ENVIRONMENT_DIR, language_version) return ( venv is None or ( - _read_state(cmd_runner, venv) == _state(additional_deps) and - language.healthy(cmd_runner, language_version) + _read_state(prefix, venv) == _state(additional_deps) and + language.healthy(prefix, language_version) ) ) def _install_all(venvs, repo_url, store): - """Tuple of (cmd_runner, language, version, deps)""" + """Tuple of (prefix, language, version, deps)""" def _need_installed(): return tuple( - (cmd_runner, language_name, version, deps) - for cmd_runner, language_name, version, deps in venvs - if not _installed(cmd_runner, language_name, version, deps) + (prefix, language_name, version, deps) + for prefix, language_name, version, deps in venvs + if not _installed(prefix, language_name, version, deps) ) if not _need_installed(): @@ -90,19 +90,19 @@ def _need_installed(): logger.info('Once installed this environment will be reused.') logger.info('This may take a few minutes...') - for cmd_runner, language_name, version, deps in need_installed: + for prefix, language_name, version, deps in need_installed: language = languages[language_name] venv = environment_dir(language.ENVIRONMENT_DIR, version) # There's potentially incomplete cleanup from previous runs # Clean it up! - if cmd_runner.exists(venv): - shutil.rmtree(cmd_runner.path(venv)) + if prefix.exists(venv): + shutil.rmtree(prefix.path(venv)) - language.install_environment(cmd_runner, version, deps) + language.install_environment(prefix, version, deps) # Write our state to indicate we're installed state = _state(deps) - _write_state(cmd_runner, venv, state) + _write_state(prefix, venv, state) def _hook(*hook_dicts): @@ -156,11 +156,11 @@ def _repo_path(self): ) @cached_property - def _cmd_runner(self): - return PrefixedCommandRunner(self._repo_path) + def _prefix(self): + return Prefix(self._repo_path) - def _cmd_runner_from_deps(self, language_name, deps): - return self._cmd_runner + def _prefix_from_deps(self, language_name, deps): + return self._prefix @cached_property def manifest_hooks(self): @@ -194,7 +194,7 @@ def _venvs(self): ) ret = [] for (language, version), deps in deps_dict.items(): - ret.append((self._cmd_runner, language, version, deps)) + ret.append((self._prefix, language, version, deps)) return tuple(ret) def require_installed(self): @@ -211,20 +211,20 @@ def run_hook(self, hook, file_args): self.require_installed() language_name = hook['language'] deps = hook['additional_dependencies'] - cmd_runner = self._cmd_runner_from_deps(language_name, deps) - return languages[language_name].run_hook(cmd_runner, hook, file_args) + prefix = self._prefix_from_deps(language_name, deps) + return languages[language_name].run_hook(prefix, hook, file_args) class LocalRepository(Repository): - def _cmd_runner_from_deps(self, language_name, deps): - """local repositories have a cmd runner per hook""" + def _prefix_from_deps(self, language_name, deps): + """local repositories have a prefix per hook""" language = languages[language_name] # pcre / pygrep / script / system / docker_image do not have # environments so they work out of the current directory if language.ENVIRONMENT_DIR is None: - return PrefixedCommandRunner(git.get_root()) + return Prefix(git.get_root()) else: - return PrefixedCommandRunner(self.store.make_local(deps)) + return Prefix(self.store.make_local(deps)) @cached_property def manifest(self): @@ -245,7 +245,7 @@ def _venvs(self): version = hook['language_version'] deps = hook['additional_dependencies'] ret.append(( - self._cmd_runner_from_deps(language, deps), + self._prefix_from_deps(language, deps), language, version, deps, )) return tuple(ret) diff --git a/tests/conftest.py b/tests/conftest.py index 36743d885..fe710e658 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,7 +14,6 @@ import pre_commit.constants as C from pre_commit import output from pre_commit.logging_handler import add_logging_handler -from pre_commit.prefixed_command_runner import PrefixedCommandRunner from pre_commit.runner import Runner from pre_commit.store import Store from pre_commit.util import cmd_output @@ -155,11 +154,6 @@ def store(tempdir_factory): yield Store(os.path.join(tempdir_factory.get(), '.pre-commit')) -@pytest.yield_fixture -def cmd_runner(tempdir_factory): - yield PrefixedCommandRunner(tempdir_factory.get()) - - @pytest.yield_fixture def runner_with_mocked_store(mock_out_store_directory): yield Runner('/', C.CONFIG_FILE) diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index 95cec1044..6e3ab6622 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -11,7 +11,7 @@ @pytest.mark.parametrize('language', all_languages) def test_install_environment_argspec(language): expected_argspec = inspect.ArgSpec( - args=['repo_cmd_runner', 'version', 'additional_dependencies'], + args=['prefix', 'version', 'additional_dependencies'], varargs=None, keywords=None, defaults=None, ) argspec = inspect.getargspec(languages[language].install_environment) @@ -26,7 +26,7 @@ def test_ENVIRONMENT_DIR(language): @pytest.mark.parametrize('language', all_languages) def test_run_hook_argpsec(language): expected_argspec = inspect.ArgSpec( - args=['repo_cmd_runner', 'hook', 'file_args'], + args=['prefix', 'hook', 'file_args'], varargs=None, keywords=None, defaults=None, ) argspec = inspect.getargspec(languages[language].run_hook) @@ -45,7 +45,7 @@ def test_get_default_version_argspec(language): @pytest.mark.parametrize('language', all_languages) def test_healthy_argspec(language): expected_argspec = inspect.ArgSpec( - args=['repo_cmd_runner', 'language_version'], + args=['prefix', 'language_version'], varargs=None, keywords=None, defaults=None, ) argspec = inspect.getargspec(languages[language].healthy) diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 1eddea1d5..bcaf0986c 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -1,39 +1,42 @@ from __future__ import unicode_literals import os.path +import pipes from pre_commit.languages.ruby import _install_rbenv +from pre_commit.prefix import Prefix +from pre_commit.util import cmd_output from testing.util import xfailif_windows_no_ruby @xfailif_windows_no_ruby -def test_install_rbenv(cmd_runner): - _install_rbenv(cmd_runner) +def test_install_rbenv(tempdir_factory): + prefix = Prefix(tempdir_factory.get()) + _install_rbenv(prefix) # Should have created rbenv directory - assert os.path.exists(cmd_runner.path('rbenv-default')) + assert os.path.exists(prefix.path('rbenv-default')) # We should have created our `activate` script - activate_path = cmd_runner.path('rbenv-default', 'bin', 'activate') + activate_path = prefix.path('rbenv-default', 'bin', 'activate') assert os.path.exists(activate_path) # Should be able to activate using our script and access rbenv - cmd_runner.run( - [ - 'bash', - '-c', - ". '{prefix}rbenv-default/bin/activate' && rbenv --help", - ], + cmd_output( + 'bash', '-c', + '. {} && rbenv --help'.format(pipes.quote(prefix.path( + 'rbenv-default', 'bin', 'activate', + ))), ) @xfailif_windows_no_ruby -def test_install_rbenv_with_version(cmd_runner): - _install_rbenv(cmd_runner, version='1.9.3p547') +def test_install_rbenv_with_version(tempdir_factory): + prefix = Prefix(tempdir_factory.get()) + _install_rbenv(prefix, version='1.9.3p547') # Should be able to activate and use rbenv install - cmd_runner.run( - [ - 'bash', - '-c', - ". '{prefix}rbenv-1.9.3p547/bin/activate' && rbenv install --help", - ], + cmd_output( + 'bash', '-c', + '. {} && rbenv install --help'.format(pipes.quote(prefix.path( + 'rbenv-1.9.3p547', 'bin', 'activate', + ))), ) diff --git a/tests/prefix_test.py b/tests/prefix_test.py new file mode 100644 index 000000000..05f3f8a4c --- /dev/null +++ b/tests/prefix_test.py @@ -0,0 +1,57 @@ +from __future__ import unicode_literals + +import os + +import pytest + +from pre_commit.prefix import Prefix + + +def norm_slash(*args): + return tuple(x.replace('/', os.sep) for x in args) + + +@pytest.mark.parametrize( + ('input', 'expected_prefix'), ( + norm_slash('.', './'), + norm_slash('foo', 'foo/'), + norm_slash('bar/', 'bar/'), + norm_slash('foo/bar', 'foo/bar/'), + norm_slash('foo/bar/', 'foo/bar/'), + ), +) +def test_init_normalizes_path_endings(input, expected_prefix): + instance = Prefix(input) + assert instance.prefix_dir == expected_prefix + + +PATH_TESTS = ( + norm_slash('foo', '', 'foo'), + norm_slash('foo', 'bar', 'foo/bar'), + norm_slash('foo/bar', '../baz', 'foo/baz'), + norm_slash('./', 'bar', 'bar'), + norm_slash('./', '', '.'), + norm_slash('/tmp/foo', '/tmp/bar', '/tmp/bar'), +) + + +@pytest.mark.parametrize(('prefix', 'path_end', 'expected_output'), PATH_TESTS) +def test_path(prefix, path_end, expected_output): + instance = Prefix(prefix) + ret = instance.path(path_end) + assert ret == expected_output + + +def test_path_multiple_args(): + instance = Prefix('foo') + ret = instance.path('bar', 'baz') + assert ret == os.path.join('foo', 'bar', 'baz') + + +def test_exists_does_not_exist(tmpdir): + assert not Prefix(str(tmpdir)).exists('foo') + + +def test_exists_does_exist(tmpdir): + tmpdir.ensure('foo') + assert Prefix(str(tmpdir)).exists('foo') diff --git a/tests/prefixed_command_runner_test.py b/tests/prefixed_command_runner_test.py deleted file mode 100644 index c928dc8af..000000000 --- a/tests/prefixed_command_runner_test.py +++ /dev/null @@ -1,133 +0,0 @@ -from __future__ import unicode_literals - -import os -import subprocess - -import mock -import pytest - -from pre_commit.prefixed_command_runner import PrefixedCommandRunner -from pre_commit.util import CalledProcessError - - -def norm_slash(input_tup): - return tuple(x.replace('/', os.sep) for x in input_tup) - - -def test_CalledProcessError_str(): - error = CalledProcessError( - 1, [str('git'), str('status')], 0, (str('stdout'), str('stderr')), - ) - assert str(error) == ( - "Command: ['git', 'status']\n" - "Return code: 1\n" - "Expected return code: 0\n" - "Output: \n" - " stdout\n" - "Errors: \n" - " stderr\n" - ) - - -def test_CalledProcessError_str_nooutput(): - error = CalledProcessError( - 1, [str('git'), str('status')], 0, (str(''), str('')), - ) - assert str(error) == ( - "Command: ['git', 'status']\n" - "Return code: 1\n" - "Expected return code: 0\n" - "Output: (none)\n" - "Errors: (none)\n" - ) - - -@pytest.fixture -def popen_mock(): - popen = mock.Mock(spec=subprocess.Popen) - popen.return_value.communicate.return_value = (b'stdout', b'stderr') - return popen - - -@pytest.fixture -def makedirs_mock(): - return mock.Mock(spec=os.makedirs) - - -@pytest.mark.parametrize( - ('input', 'expected_prefix'), ( - norm_slash(('.', './')), - norm_slash(('foo', 'foo/')), - norm_slash(('bar/', 'bar/')), - norm_slash(('foo/bar', 'foo/bar/')), - norm_slash(('foo/bar/', 'foo/bar/')), - ), -) -def test_init_normalizes_path_endings(input, expected_prefix): - input = input.replace('/', os.sep) - expected_prefix = expected_prefix.replace('/', os.sep) - instance = PrefixedCommandRunner(input) - assert instance.prefix_dir == expected_prefix - - -def test_run_substitutes_prefix(popen_mock, makedirs_mock): - instance = PrefixedCommandRunner( - 'prefix', popen=popen_mock, makedirs=makedirs_mock, - ) - ret = instance.run(['{prefix}bar', 'baz'], retcode=None) - popen_mock.assert_called_once_with( - (str(os.path.join('prefix', 'bar')), str('baz')), - env=None, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - assert ret == (popen_mock.return_value.returncode, 'stdout', 'stderr') - - -PATH_TESTS = ( - norm_slash(('foo', '', 'foo')), - norm_slash(('foo', 'bar', 'foo/bar')), - norm_slash(('foo/bar', '../baz', 'foo/baz')), - norm_slash(('./', 'bar', 'bar')), - norm_slash(('./', '', '.')), - norm_slash(('/tmp/foo', '/tmp/bar', '/tmp/bar')), -) - - -@pytest.mark.parametrize(('prefix', 'path_end', 'expected_output'), PATH_TESTS) -def test_path(prefix, path_end, expected_output): - instance = PrefixedCommandRunner(prefix) - ret = instance.path(path_end) - assert ret == expected_output - - -def test_path_multiple_args(): - instance = PrefixedCommandRunner('foo') - ret = instance.path('bar', 'baz') - assert ret == os.path.join('foo', 'bar', 'baz') - - -def test_create_path_if_not_exists(in_tmpdir): - instance = PrefixedCommandRunner('foo') - assert not os.path.exists('foo') - instance._create_path_if_not_exists() - assert os.path.exists('foo') - - -def test_exists_does_not_exist(in_tmpdir): - assert not PrefixedCommandRunner('.').exists('foo') - - -def test_exists_does_exist(in_tmpdir): - os.mkdir('foo') - assert PrefixedCommandRunner('.').exists('foo') - - -def test_raises_on_error(popen_mock, makedirs_mock): - popen_mock.return_value.returncode = 1 - with pytest.raises(CalledProcessError): - instance = PrefixedCommandRunner( - '.', popen=popen_mock, makedirs=makedirs_mock, - ) - instance.run(['echo']) diff --git a/tests/repository_test.py b/tests/repository_test.py index 1d38d2466..1c518eba8 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -191,7 +191,7 @@ def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): def test_run_a_node_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'node_hooks_repo', - 'foo', ['/dev/null'], b'Hello World\n', + 'foo', [os.devnull], b'Hello World\n', ) @@ -200,7 +200,7 @@ def test_run_a_node_hook(tempdir_factory, store): def test_run_versioned_node_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'node_0_11_8_hooks_repo', - 'node-11-8-hook', ['/dev/null'], b'v0.11.8\nHello World\n', + 'node-11-8-hook', [os.devnull], b'v0.11.8\nHello World\n', ) @@ -209,7 +209,7 @@ def test_run_versioned_node_hook(tempdir_factory, store): def test_run_a_ruby_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'ruby_hooks_repo', - 'ruby_hook', ['/dev/null'], b'Hello world from a ruby hook\n', + 'ruby_hook', [os.devnull], b'Hello world from a ruby hook\n', ) @@ -219,7 +219,7 @@ def test_run_versioned_ruby_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'ruby_versioned_hooks_repo', 'ruby_hook', - ['/dev/null'], + [os.devnull], b'2.1.5\nHello world from a ruby hook\n', ) @@ -242,7 +242,7 @@ def test_run_ruby_hook_with_disable_shared_gems( _test_hook_repo( tempdir_factory, store, 'ruby_versioned_hooks_repo', 'ruby_hook', - ['/dev/null'], + [os.devnull], b'2.1.5\nHello world from a ruby hook\n', ) @@ -251,7 +251,7 @@ def test_run_ruby_hook_with_disable_shared_gems( def test_system_hook_with_spaces(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'system_hook_with_spaces_repo', - 'system-hook-with-spaces', ['/dev/null'], b'Hello World\n', + 'system-hook-with-spaces', [os.devnull], b'Hello World\n', ) @@ -276,7 +276,7 @@ def test_golang_hook(tempdir_factory, store): def test_missing_executable(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'not_found_exe', - 'not-found-exe', ['/dev/null'], + 'not-found-exe', [os.devnull], b'Executable `i-dont-exist-lol` not found', expected_return_code=1, ) @@ -424,7 +424,7 @@ def test_cwd_of_hook(tempdir_factory, store): def test_lots_of_files(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'script_hooks_repo', - 'bash_hook', ['/dev/null'] * 15000, mock.ANY, + 'bash_hook', [os.devnull] * 15000, mock.ANY, ) @@ -467,7 +467,7 @@ def test_additional_python_dependencies_installed(tempdir_factory, store): config['hooks'][0]['additional_dependencies'] = ['mccabe'] repo = Repository.create(config, store) repo.require_installed() - with python.in_env(repo._cmd_runner, 'default'): + with python.in_env(repo._prefix, 'default'): output = cmd_output('pip', 'freeze', '-l')[1] assert 'mccabe' in output @@ -484,7 +484,7 @@ def test_additional_dependencies_roll_forward(tempdir_factory, store): repo = Repository.create(config, store) repo.require_installed() # We should see our additional dependency installed - with python.in_env(repo._cmd_runner, 'default'): + with python.in_env(repo._prefix, 'default'): output = cmd_output('pip', 'freeze', '-l')[1] assert 'mccabe' in output @@ -499,7 +499,7 @@ def test_additional_ruby_dependencies_installed( config['hooks'][0]['additional_dependencies'] = ['thread_safe', 'tins'] repo = Repository.create(config, store) repo.require_installed() - with ruby.in_env(repo._cmd_runner, 'default'): + with ruby.in_env(repo._prefix, 'default'): output = cmd_output('gem', 'list', '--local')[1] assert 'thread_safe' in output assert 'tins' in output @@ -516,7 +516,7 @@ def test_additional_node_dependencies_installed( config['hooks'][0]['additional_dependencies'] = ['lodash'] repo = Repository.create(config, store) repo.require_installed() - with node.in_env(repo._cmd_runner, 'default'): + with node.in_env(repo._prefix, 'default'): cmd_output('npm', 'config', 'set', 'global', 'true') output = cmd_output('npm', 'ls')[1] assert 'lodash' in output @@ -533,7 +533,7 @@ def test_additional_golang_dependencies_installed( config['hooks'][0]['additional_dependencies'] = deps repo = Repository.create(config, store) repo.require_installed() - binaries = os.listdir(repo._cmd_runner.path( + binaries = os.listdir(repo._prefix.path( helpers.environment_dir(golang.ENVIRONMENT_DIR, 'default'), 'bin', )) # normalize for windows @@ -600,7 +600,7 @@ class MyKeyboardInterrupt(KeyboardInterrupt): # Should have made an environment, however this environment is broken! envdir = 'py_env-{}'.format(python.get_default_version()) - assert repo._cmd_runner.exists(envdir) + assert repo._prefix.exists(envdir) # However, it should be perfectly runnable (reinstall after botched # install) @@ -618,7 +618,7 @@ def test_invalidated_virtualenv(tempdir_factory, store): # Simulate breaking of the virtualenv repo.require_installed() version = python.get_default_version() - libdir = repo._cmd_runner.path('py_env-{}'.format(version), 'lib', version) + libdir = repo._prefix.path('py_env-{}'.format(version), 'lib', version) paths = [ os.path.join(libdir, p) for p in ('site.py', 'site.pyc', '__pycache__') ] diff --git a/tests/util_test.py b/tests/util_test.py index ba2b4a821..156148d58 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -5,6 +5,7 @@ import pytest +from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import cwd @@ -12,6 +13,34 @@ from pre_commit.util import tmpdir +def test_CalledProcessError_str(): + error = CalledProcessError( + 1, [str('git'), str('status')], 0, (str('stdout'), str('stderr')), + ) + assert str(error) == ( + "Command: ['git', 'status']\n" + "Return code: 1\n" + "Expected return code: 0\n" + "Output: \n" + " stdout\n" + "Errors: \n" + " stderr\n" + ) + + +def test_CalledProcessError_str_nooutput(): + error = CalledProcessError( + 1, [str('git'), str('status')], 0, (str(''), str('')), + ) + assert str(error) == ( + "Command: ['git', 'status']\n" + "Return code: 1\n" + "Expected return code: 0\n" + "Output: (none)\n" + "Errors: (none)\n" + ) + + @pytest.fixture def memoized_by_cwd(): @memoize_by_cwd From b4541d8a5ff8a8e1eafbdd61021039d7e4311ec0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 11 Jan 2018 22:20:21 -0800 Subject: [PATCH 136/544] Update the versioned node hook test --- .../resources/node_0_11_8_hooks_repo/.pre-commit-hooks.yaml | 6 ------ testing/resources/node_0_11_8_hooks_repo/package.json | 5 ----- .../node_versioned_hooks_repo/.pre-commit-hooks.yaml | 6 ++++++ .../bin/main.js | 0 testing/resources/node_versioned_hooks_repo/package.json | 5 +++++ tests/repository_test.py | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) delete mode 100644 testing/resources/node_0_11_8_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/node_0_11_8_hooks_repo/package.json create mode 100644 testing/resources/node_versioned_hooks_repo/.pre-commit-hooks.yaml rename testing/resources/{node_0_11_8_hooks_repo => node_versioned_hooks_repo}/bin/main.js (100%) create mode 100644 testing/resources/node_versioned_hooks_repo/package.json diff --git a/testing/resources/node_0_11_8_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/node_0_11_8_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 005a1e3b0..000000000 --- a/testing/resources/node_0_11_8_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,6 +0,0 @@ -- id: node-11-8-hook - name: Node 0.11.8 hook - entry: node-11-8-hook - language: node - language_version: 0.11.8 - files: \.js$ diff --git a/testing/resources/node_0_11_8_hooks_repo/package.json b/testing/resources/node_0_11_8_hooks_repo/package.json deleted file mode 100644 index 911a3ed9e..000000000 --- a/testing/resources/node_0_11_8_hooks_repo/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "node-11-8-hook", - "version": "0.0.1", - "bin": {"node-11-8-hook": "./bin/main.js"} -} diff --git a/testing/resources/node_versioned_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/node_versioned_hooks_repo/.pre-commit-hooks.yaml new file mode 100644 index 000000000..e7ad5ea7b --- /dev/null +++ b/testing/resources/node_versioned_hooks_repo/.pre-commit-hooks.yaml @@ -0,0 +1,6 @@ +- id: versioned-node-hook + name: Versioned node hook + entry: versioned-node-hook + language: node + language_version: 9.3.0 + files: \.js$ diff --git a/testing/resources/node_0_11_8_hooks_repo/bin/main.js b/testing/resources/node_versioned_hooks_repo/bin/main.js similarity index 100% rename from testing/resources/node_0_11_8_hooks_repo/bin/main.js rename to testing/resources/node_versioned_hooks_repo/bin/main.js diff --git a/testing/resources/node_versioned_hooks_repo/package.json b/testing/resources/node_versioned_hooks_repo/package.json new file mode 100644 index 000000000..18c7787c7 --- /dev/null +++ b/testing/resources/node_versioned_hooks_repo/package.json @@ -0,0 +1,5 @@ +{ + "name": "versioned-node-hook", + "version": "0.0.1", + "bin": {"versioned-node-hook": "./bin/main.js"} +} diff --git a/tests/repository_test.py b/tests/repository_test.py index 1c518eba8..0e43e728a 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -199,8 +199,8 @@ def test_run_a_node_hook(tempdir_factory, store): @pytest.mark.integration def test_run_versioned_node_hook(tempdir_factory, store): _test_hook_repo( - tempdir_factory, store, 'node_0_11_8_hooks_repo', - 'node-11-8-hook', [os.devnull], b'v0.11.8\nHello World\n', + tempdir_factory, store, 'node_versioned_hooks_repo', + 'versioned-node-hook', [os.devnull], b'v9.3.0\nHello World\n', ) From 6e46d6ae75072ae2266408868cff07338507359f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 11 Jan 2018 22:25:39 -0800 Subject: [PATCH 137/544] Support node on windows with long path hack --- pre_commit/languages/node.py | 35 ++++++++++++++++++++--------------- testing/util.py | 19 ++++++++++++++++--- tests/repository_test.py | 8 ++++---- tox.ini | 2 +- 4 files changed, 41 insertions(+), 23 deletions(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 5f5d9fdd5..4779db509 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -7,6 +7,7 @@ from pre_commit.envcontext import envcontext from pre_commit.envcontext import Var from pre_commit.languages import helpers +from pre_commit.languages.python import bin_dir from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.xargs import xargs @@ -17,10 +18,17 @@ healthy = helpers.basic_healthy -def get_env_patch(venv): # pragma: windows no cover +def _envdir(prefix, version): + directory = helpers.environment_dir(ENVIRONMENT_DIR, version) + return prefix.path(directory) + + +def get_env_patch(venv): if sys.platform == 'cygwin': # pragma: no cover _, win_venv, _ = cmd_output('cygpath', '-w', venv) install_prefix = r'{}\bin'.format(win_venv.strip()) + elif sys.platform == 'win32': # pragma: no cover + install_prefix = bin_dir(venv) else: install_prefix = venv return ( @@ -28,29 +36,26 @@ def get_env_patch(venv): # pragma: windows no cover ('NPM_CONFIG_PREFIX', install_prefix), ('npm_config_prefix', install_prefix), ('NODE_PATH', os.path.join(venv, 'lib', 'node_modules')), - ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), + ('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))), ) @contextlib.contextmanager -def in_env(prefix, language_version): # pragma: windows no cover - envdir = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, language_version), - ) - with envcontext(get_env_patch(envdir)): +def in_env(prefix, language_version): + with envcontext(get_env_patch(_envdir(prefix, language_version))): yield -def install_environment( - prefix, version, additional_dependencies, -): # pragma: windows no cover +def install_environment(prefix, version, additional_dependencies): additional_dependencies = tuple(additional_dependencies) assert prefix.exists('package.json') - directory = helpers.environment_dir(ENVIRONMENT_DIR, version) + envdir = _envdir(prefix, version) - env_dir = prefix.path(directory) - with clean_path_on_failure(env_dir): - cmd = [sys.executable, '-m', 'nodeenv', '--prebuilt', env_dir] + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx?f=255&MSPPError=-2147217396#maxpath + if sys.platform == 'win32': # pragma: no cover + envdir = '\\\\?\\' + os.path.normpath(envdir) + with clean_path_on_failure(envdir): + cmd = [sys.executable, '-m', 'nodeenv', '--prebuilt', envdir] if version != 'default': cmd.extend(['-n', version]) cmd_output(*cmd) @@ -62,6 +67,6 @@ def install_environment( ) -def run_hook(prefix, hook, file_args): # pragma: windows no cover +def run_hook(prefix, hook, file_args): with in_env(prefix, hook['language_version']): return xargs(helpers.to_cmd(hook), file_args) diff --git a/testing/util.py b/testing/util.py index 357968fb8..aa4b76f50 100644 --- a/testing/util.py +++ b/testing/util.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import os.path +import sys import pytest @@ -42,9 +43,21 @@ def cmd_output_mocked_pre_commit_home(*args, **kwargs): reason='Ruby support not yet implemented on windows.', ) -xfailif_windows_no_node = pytest.mark.xfail( - os.name == 'nt', - reason='Node support not yet implemented on windows.', + +def broken_deep_listdir(): # pragma: no cover (platform specific) + if sys.platform != 'win32': + return False + try: + os.listdir(str('\\\\?\\') + os.path.abspath(str('.'))) + except OSError: + return True + else: + return False + + +xfailif_broken_deep_listdir = pytest.mark.xfail( + broken_deep_listdir(), + reason='Node on windows requires deep listdir', ) diff --git a/tests/repository_test.py b/tests/repository_test.py index 0e43e728a..c160581e6 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -31,8 +31,8 @@ from testing.util import get_resource_path from testing.util import skipif_cant_run_docker from testing.util import skipif_cant_run_swift +from testing.util import xfailif_broken_deep_listdir from testing.util import xfailif_no_pcre_support -from testing.util import xfailif_windows_no_node from testing.util import xfailif_windows_no_ruby @@ -186,7 +186,7 @@ def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): ) -@xfailif_windows_no_node +@xfailif_broken_deep_listdir @pytest.mark.integration def test_run_a_node_hook(tempdir_factory, store): _test_hook_repo( @@ -195,7 +195,7 @@ def test_run_a_node_hook(tempdir_factory, store): ) -@xfailif_windows_no_node +@xfailif_broken_deep_listdir @pytest.mark.integration def test_run_versioned_node_hook(tempdir_factory, store): _test_hook_repo( @@ -505,7 +505,7 @@ def test_additional_ruby_dependencies_installed( assert 'tins' in output -@xfailif_windows_no_node +@xfailif_broken_deep_listdir @pytest.mark.integration def test_additional_node_dependencies_installed( tempdir_factory, store, diff --git a/tox.ini b/tox.ini index 872b4c359..a254c3698 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ envlist = py27,py35,py36,pypy [testenv] deps = -rrequirements-dev.txt -passenv = GOROOT HOME HOMEPATH PROGRAMDATA TERM +passenv = GOROOT HOME HOMEPATH PROCESSOR_ARCHITECTURE PROGRAMDATA TERM commands = coverage erase coverage run -m pytest {posargs:tests} From 8fb644e7c0d738fcd09cc1a61d035c888bc40005 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 13 Jan 2018 15:53:22 -0800 Subject: [PATCH 138/544] Simplify prefix a bit --- pre_commit/languages/script.py | 2 +- pre_commit/prefix.py | 10 ++++----- tests/prefix_test.py | 37 +++++++++------------------------- 3 files changed, 15 insertions(+), 34 deletions(-) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 8186e77ab..551b4d80e 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -12,5 +12,5 @@ def run_hook(prefix, hook, file_args): cmd = helpers.to_cmd(hook) - cmd = (prefix.prefix_dir + cmd[0],) + cmd[1:] + cmd = (prefix.path(cmd[0]),) + cmd[1:] return xargs(cmd, file_args) diff --git a/pre_commit/prefix.py b/pre_commit/prefix.py index 128bd861b..073b3f542 100644 --- a/pre_commit/prefix.py +++ b/pre_commit/prefix.py @@ -5,16 +5,14 @@ class Prefix(object): def __init__(self, prefix_dir): - self.prefix_dir = prefix_dir.rstrip(os.sep) + os.sep + self.prefix_dir = prefix_dir def path(self, *parts): - path = os.path.join(self.prefix_dir, *parts) - return os.path.normpath(path) + return os.path.normpath(os.path.join(self.prefix_dir, *parts)) def exists(self, *parts): return os.path.exists(self.path(*parts)) def star(self, end): - return tuple( - path for path in os.listdir(self.prefix_dir) if path.endswith(end) - ) + paths = os.listdir(self.prefix_dir) + return tuple(path for path in paths if path.endswith(end)) diff --git a/tests/prefix_test.py b/tests/prefix_test.py index 05f3f8a4c..728b5df42 100644 --- a/tests/prefix_test.py +++ b/tests/prefix_test.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -import os +import os.path import pytest @@ -12,30 +12,16 @@ def norm_slash(*args): @pytest.mark.parametrize( - ('input', 'expected_prefix'), ( - norm_slash('.', './'), - norm_slash('foo', 'foo/'), - norm_slash('bar/', 'bar/'), - norm_slash('foo/bar', 'foo/bar/'), - norm_slash('foo/bar/', 'foo/bar/'), + ('prefix', 'path_end', 'expected_output'), + ( + norm_slash('foo', '', 'foo'), + norm_slash('foo', 'bar', 'foo/bar'), + norm_slash('foo/bar', '../baz', 'foo/baz'), + norm_slash('./', 'bar', 'bar'), + norm_slash('./', '', '.'), + norm_slash('/tmp/foo', '/tmp/bar', '/tmp/bar'), ), ) -def test_init_normalizes_path_endings(input, expected_prefix): - instance = Prefix(input) - assert instance.prefix_dir == expected_prefix - - -PATH_TESTS = ( - norm_slash('foo', '', 'foo'), - norm_slash('foo', 'bar', 'foo/bar'), - norm_slash('foo/bar', '../baz', 'foo/baz'), - norm_slash('./', 'bar', 'bar'), - norm_slash('./', '', '.'), - norm_slash('/tmp/foo', '/tmp/bar', '/tmp/bar'), -) - - -@pytest.mark.parametrize(('prefix', 'path_end', 'expected_output'), PATH_TESTS) def test_path(prefix, path_end, expected_output): instance = Prefix(prefix) ret = instance.path(path_end) @@ -48,10 +34,7 @@ def test_path_multiple_args(): assert ret == os.path.join('foo', 'bar', 'baz') -def test_exists_does_not_exist(tmpdir): +def test_exists(tmpdir): assert not Prefix(str(tmpdir)).exists('foo') - - -def test_exists_does_exist(tmpdir): tmpdir.ensure('foo') assert Prefix(str(tmpdir)).exists('foo') From d5dcebf6712aab0b14ec3b6967e5f203d7972eb2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 13 Jan 2018 17:28:19 -0800 Subject: [PATCH 139/544] Deprecate the pcre language --- pre_commit/commands/run.py | 9 +++++++++ tests/commands/run_test.py | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index c70eff011..a16d8fe11 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -67,6 +67,15 @@ def _run_single_hook(filenames, hook, repo, args, skips, cols): filenames = _filter_by_include_exclude(filenames, include, exclude) types, exclude_types = hook['types'], hook['exclude_types'] filenames = _filter_by_types(filenames, types, exclude_types) + + if hook['language'] == 'pcre': + logger.warning( + '`{}` (from {}) uses the deprecated pcre language.\n' + 'The pcre language is scheduled for removal in pre-commit 2.x.\n' + 'The pygrep language is a more portable (and usually drop-in) ' + 'replacement.'.format(hook['id'], repo.repo_config['repo']), + ) + if hook['id'] in skips: output.write(get_hook_message( _hook_msg_start(hook, args.verbose), diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 97c82c251..cbf4e9817 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -529,7 +529,7 @@ def test_push_hook(cap_out, repo_with_passing_hook, mock_out_store_directory): ('id', 'do_not_commit'), ('name', 'hook 2'), ('entry', 'DO NOT COMMIT'), - ('language', 'pcre'), + ('language', 'pygrep'), ('types', ['text']), ('stages', ['push']), )), @@ -592,7 +592,7 @@ def test_local_hook_passes( ('id', 'do_not_commit'), ('name', 'Block if "DO NOT COMMIT" is found'), ('entry', 'DO NOT COMMIT'), - ('language', 'pcre'), + ('language', 'pygrep'), ('files', '^(.*)$'), )), ), @@ -645,6 +645,35 @@ def test_local_hook_fails( ) +def test_pcre_deprecation_warning( + cap_out, repo_with_passing_hook, mock_out_store_directory, +): + config = OrderedDict(( + ('repo', 'local'), + ( + 'hooks', [OrderedDict(( + ('id', 'pcre-hook'), + ('name', 'pcre-hook'), + ('language', 'pcre'), + ('entry', '.'), + ))], + ), + )) + add_config_to_repo(repo_with_passing_hook, config) + + _test_run( + cap_out, + repo_with_passing_hook, + opts={}, + expected_outputs=[ + b'[WARNING] `pcre-hook` (from local) uses the deprecated ' + b'pcre language.', + ], + expected_ret=0, + stage=False, + ) + + def test_meta_hook_passes( cap_out, repo_with_passing_hook, mock_out_store_directory, ): From 5a4dc0ce30348dbe493fa6d67455c46ef30d8b39 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 13 Jan 2018 18:17:54 -0800 Subject: [PATCH 140/544] https-ify links - A lot of http links loaded fine on https - pre-commit.com is now loadable on https via cloudflare --- CHANGELOG.md | 4 ++-- CONTRIBUTING.md | 2 +- README.md | 2 +- pre_commit/commands/sample_config.py | 4 ++-- pre_commit/main.py | 2 +- pre_commit/parse_shebang.py | 2 +- pre_commit/store.py | 2 +- tests/commands/sample_config_test.py | 4 ++-- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b1ae8499..4ec7d541b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -158,7 +158,7 @@ 1.0.0 ===== -pre-commit will now be following [semver](http://semver.org/). Thanks to all +pre-commit will now be following [semver](https://semver.org/). Thanks to all of the [contributors](https://github.com/pre-commit/pre-commit/graphs/contributors) that have helped us get this far! @@ -561,7 +561,7 @@ that have helped us get this far! 0.3.5 ===== -- Support running during `pre-push`. See http://pre-commit.com/#advanced 'pre-commit during push'. +- Support running during `pre-push`. See https://pre-commit.com/#advanced 'pre-commit during push'. 0.3.4 ===== diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index da27dec69..e9a9f9e38 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,7 +44,7 @@ With the environment activated simply run `pre-commit install`. ## Documentation -Documentation is hosted at http://pre-commit.com +Documentation is hosted at https://pre-commit.com This website is controlled through https://github.com/pre-commit/pre-commit.github.io diff --git a/README.md b/README.md index 8bbc534b1..12b222d3b 100644 --- a/README.md +++ b/README.md @@ -6,4 +6,4 @@ A framework for managing and maintaining multi-language pre-commit hooks. -For more information see: http://pre-commit.com/ +For more information see: https://pre-commit.com/ diff --git a/pre_commit/commands/sample_config.py b/pre_commit/commands/sample_config.py index c8c3bf107..ae5946856 100644 --- a/pre_commit/commands/sample_config.py +++ b/pre_commit/commands/sample_config.py @@ -8,8 +8,8 @@ # significantly faster than https:// or http://). For now, periodically # manually updating the revision is fine. SAMPLE_CONFIG = '''\ -# See http://pre-commit.com for more information -# See http://pre-commit.com/hooks.html for more hooks +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks sha: v0.9.2 diff --git a/pre_commit/main.py b/pre_commit/main.py index 865571a53..16b6c3b61 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -94,7 +94,7 @@ def main(argv=None): argv = [five.to_text(arg) for arg in argv] parser = argparse.ArgumentParser() - # http://stackoverflow.com/a/8521644/812183 + # https://stackoverflow.com/a/8521644/812183 parser.add_argument( '-V', '--version', action='version', diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index 4419cbfc5..33326819e 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -56,7 +56,7 @@ def normexe(orig_exe): def normalize_cmd(cmd): """Fixes for the following issues on windows - - http://bugs.python.org/issue8557 + - https://bugs.python.org/issue8557 - windows does not parse shebangs This function also makes deep-path shebangs work just fine diff --git a/pre_commit/store.py b/pre_commit/store.py index 9c6734526..131198407 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -68,7 +68,7 @@ def _write_sqlite_db(self): os.close(fd) # sqlite doesn't close its fd with its contextmanager >.< # contextlib.closing fixes this. - # See: http://stackoverflow.com/a/28032829/812183 + # See: https://stackoverflow.com/a/28032829/812183 with contextlib.closing(sqlite3.connect(tmpfile)) as db: db.executescript( 'CREATE TABLE repos (' diff --git a/tests/commands/sample_config_test.py b/tests/commands/sample_config_test.py index 9d74a011b..1dca98b4c 100644 --- a/tests/commands/sample_config_test.py +++ b/tests/commands/sample_config_test.py @@ -9,8 +9,8 @@ def test_sample_config(capsys): assert ret == 0 out, _ = capsys.readouterr() assert out == '''\ -# See http://pre-commit.com for more information -# See http://pre-commit.com/hooks.html for more hooks +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks sha: v0.9.2 From 98ec74dcabd8279f99b072e4232c5042ce16924b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 13 Jan 2018 20:01:29 -0800 Subject: [PATCH 141/544] v1.5.0 --- CHANGELOG.md | 18 ++++++++++++++++++ setup.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ec7d541b..38a47120c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +1.5.0 +===== + +### Features +- pre-commit now supports node hooks on windows. + - for now, requires python3 due to https://bugs.python.org/issue32539 + - huge thanks to @wenzowski for the tip! + - #200 issue by @asottile. + - #685 PR by @asottile. + +### Misc +- internal reorganization of `PrefixedCommandRunner` -> `Prefix` + - #684 PR by @asottile. +- https-ify links. + - pre-commit.com is now served over https. + - #688 PR by @asottile. + + 1.4.5 ===== diff --git a/setup.py b/setup.py index 2eb42418e..0a0ca2246 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.4.5', + version='1.5.0', author='Anthony Sottile', author_email='asottile@umich.edu', From 0f54fedac978cfd749b0ab53bf05e2d69be93d2e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 21 Jan 2018 15:31:17 -0800 Subject: [PATCH 142/544] Replace deprecated yield_fixture with fixture Committed via https://github.com/asottile/all-repos --- tests/commands/autoupdate_test.py | 10 +++++----- tests/commands/run_test.py | 6 +++--- tests/conftest.py | 24 ++++++++++++------------ tests/error_handler_test.py | 2 +- tests/main_test.py | 6 +++--- tests/staged_files_only_test.py | 8 ++++---- 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index ee20c7dd1..91e7733ff 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -25,7 +25,7 @@ from testing.util import get_resource_path -@pytest.yield_fixture +@pytest.fixture def up_to_date_repo(tempdir_factory): yield make_repo(tempdir_factory, 'python_hooks_repo') @@ -81,7 +81,7 @@ def test_autoupdate_old_revision_broken( assert update_rev in after -@pytest.yield_fixture +@pytest.fixture def out_of_date_repo(tempdir_factory): path = make_repo(tempdir_factory, 'python_hooks_repo') original_sha = git.head_sha(path) @@ -221,7 +221,7 @@ def test_loses_formatting_when_not_detectable( assert after == expected -@pytest.yield_fixture +@pytest.fixture def tagged_repo(out_of_date_repo): with cwd(out_of_date_repo.path): cmd_output('git', 'tag', 'v1.2.3') @@ -241,7 +241,7 @@ def test_autoupdate_tagged_repo( assert 'v1.2.3' in open(C.CONFIG_FILE).read() -@pytest.yield_fixture +@pytest.fixture def tagged_repo_with_more_commits(tagged_repo): with cwd(tagged_repo.path): cmd_output('git', 'commit', '--allow-empty', '-m', 'commit!') @@ -262,7 +262,7 @@ def test_autoupdate_tags_only( assert 'v1.2.3' in open(C.CONFIG_FILE).read() -@pytest.yield_fixture +@pytest.fixture def hook_disappearing_repo(tempdir_factory): path = make_repo(tempdir_factory, 'python_hooks_repo') original_sha = git.head_sha(path) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index cbf4e9817..d800365fd 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -29,14 +29,14 @@ from testing.util import xfailif_no_symlink -@pytest.yield_fixture +@pytest.fixture def repo_with_passing_hook(tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(git_path): yield git_path -@pytest.yield_fixture +@pytest.fixture def repo_with_failing_hook(tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'failing_hook_repo') with cwd(git_path): @@ -699,7 +699,7 @@ def test_meta_hook_passes( ) -@pytest.yield_fixture +@pytest.fixture def modified_config_repo(repo_with_passing_hook): with modify_config(repo_with_passing_hook, commit=False) as config: # Some minor modification diff --git a/tests/conftest.py b/tests/conftest.py index fe710e658..fd3784dfa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,7 +23,7 @@ from testing.fixtures import write_config -@pytest.yield_fixture +@pytest.fixture def tempdir_factory(tmpdir): class TmpdirFactory(object): def __init__(self): @@ -38,7 +38,7 @@ def get(self): yield TmpdirFactory() -@pytest.yield_fixture +@pytest.fixture def in_tmpdir(tempdir_factory): path = tempdir_factory.get() with cwd(path): @@ -65,7 +65,7 @@ def _make_conflict(): cmd_output('git', 'merge', 'foo', retcode=None) -@pytest.yield_fixture +@pytest.fixture def in_merge_conflict(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): @@ -80,7 +80,7 @@ def in_merge_conflict(tempdir_factory): yield os.path.join(conflict_path) -@pytest.yield_fixture +@pytest.fixture def in_conflicting_submodule(tempdir_factory): git_dir_1 = git_dir(tempdir_factory) git_dir_2 = git_dir(tempdir_factory) @@ -116,7 +116,7 @@ def commit_msg_repo(tempdir_factory): yield path -@pytest.yield_fixture(autouse=True, scope='session') +@pytest.fixture(autouse=True, scope='session') def dont_write_to_home_directory(): """pre_commit.store.Store will by default write to the home directory We'll mock out `Store.get_default_directory` to raise invariantly so we @@ -138,7 +138,7 @@ def configure_logging(): add_logging_handler(use_color=False) -@pytest.yield_fixture +@pytest.fixture def mock_out_store_directory(tempdir_factory): tmpdir = tempdir_factory.get() with mock.patch.object( @@ -149,23 +149,23 @@ def mock_out_store_directory(tempdir_factory): yield tmpdir -@pytest.yield_fixture +@pytest.fixture def store(tempdir_factory): yield Store(os.path.join(tempdir_factory.get(), '.pre-commit')) -@pytest.yield_fixture +@pytest.fixture def runner_with_mocked_store(mock_out_store_directory): yield Runner('/', C.CONFIG_FILE) -@pytest.yield_fixture +@pytest.fixture def log_info_mock(): with mock.patch.object(logging.getLogger('pre_commit'), 'info') as mck: yield mck -@pytest.yield_fixture +@pytest.fixture def log_warning_mock(): with mock.patch.object(logging.getLogger('pre_commit'), 'warning') as mck: yield mck @@ -197,7 +197,7 @@ def get(self): return self.get_bytes().decode('UTF-8') -@pytest.yield_fixture +@pytest.fixture def cap_out(): stream = FakeStream() write = functools.partial(output.write, stream=stream) @@ -207,7 +207,7 @@ def cap_out(): yield Fixture(stream) -@pytest.yield_fixture +@pytest.fixture def fake_log_handler(): handler = mock.Mock(level=logging.INFO) logger = logging.getLogger('pre_commit') diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 0e93298b0..36eb1faf1 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -14,7 +14,7 @@ from testing.util import cmd_output_mocked_pre_commit_home -@pytest.yield_fixture +@pytest.fixture def mocked_log_and_exit(): with mock.patch.object(error_handler, '_log_and_exit') as log_and_exit: yield log_and_exit diff --git a/tests/main_test.py b/tests/main_test.py index e925cfcfd..deb3ba18a 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -19,7 +19,7 @@ CMDS = tuple(fn.replace('_', '-') for fn in FNS) -@pytest.yield_fixture +@pytest.fixture def mock_commands(): mcks = {fn: mock.patch.object(main, fn).start() for fn in FNS} ret = auto_namedtuple(**mcks) @@ -32,7 +32,7 @@ class CalledExit(Exception): pass -@pytest.yield_fixture +@pytest.fixture def argparse_exit_mock(): with mock.patch.object( argparse.ArgumentParser, 'exit', side_effect=CalledExit, @@ -40,7 +40,7 @@ def argparse_exit_mock(): yield exit_mock -@pytest.yield_fixture +@pytest.fixture def argparse_parse_args_spy(): parse_args_mock = mock.Mock() diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 36b19855c..d4dfadd60 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -30,7 +30,7 @@ def get_short_git_status(): return dict(reversed(line.split()) for line in git_status.splitlines()) -@pytest.yield_fixture +@pytest.fixture def foo_staged(tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): @@ -132,7 +132,7 @@ def test_foo_both_modify_conflicting(foo_staged, patch_dir): _test_foo_state(foo_staged, FOO_CONTENTS.replace('1', 'a'), 'AM') -@pytest.yield_fixture +@pytest.fixture def img_staged(tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): @@ -187,7 +187,7 @@ def test_img_conflict(img_staged, patch_dir): _test_img_state(img_staged, 'img2.jpg', 'AM') -@pytest.yield_fixture +@pytest.fixture def submodule_with_commits(tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): @@ -203,7 +203,7 @@ def checkout_submodule(sha): cmd_output('git', 'checkout', sha) -@pytest.yield_fixture +@pytest.fixture def sub_staged(submodule_with_commits, tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): From 1bfd108593a268bdaf961249866f958081135ce1 Mon Sep 17 00:00:00 2001 From: Sam Duke Date: Wed, 24 Jan 2018 14:01:59 +0000 Subject: [PATCH 143/544] Properly detect if commit is a root commit Fix bad check for ancestor root commits. --- pre_commit/resources/pre-push-tmpl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/resources/pre-push-tmpl b/pre_commit/resources/pre-push-tmpl index f866eeff2..0a3dad572 100644 --- a/pre_commit/resources/pre-push-tmpl +++ b/pre_commit/resources/pre-push-tmpl @@ -8,7 +8,8 @@ do if [ -n "$first_ancestor" ]; then # Check that the ancestor has at least one parent git rev-list --max-parents=0 "$local_sha" | grep "$first_ancestor" > /dev/null - if [ $? -ne 0 ]; then + if [ $? -eq 0 ]; then + # Pushing the whole tree, including the root commit, so run on all files args="--all-files" else source=$(git rev-parse "$first_ancestor"^) From 4a6fdd4abef03e72ed09c8719554390c0d2ead3b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 24 Jan 2018 09:21:44 -0800 Subject: [PATCH 144/544] Add test for pushing to unrelated upstream --- tests/commands/install_uninstall_test.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 2ba5ce36b..1659684a4 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -564,6 +564,23 @@ def test_pre_push_integration_accepted(tempdir_factory): assert 'Passed' in output +def test_pre_push_new_upstream(tempdir_factory): + upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + upstream2 = git_dir(tempdir_factory) + path = tempdir_factory.get() + cmd_output('git', 'clone', upstream, path) + with cwd(path): + install(Runner(path, C.CONFIG_FILE), hook_type='pre-push') + assert _get_commit_output(tempdir_factory)[0] == 0 + + cmd_output('git', 'remote', 'rename', 'origin', 'upstream') + cmd_output('git', 'remote', 'add', 'origin', upstream2) + retc, output = _get_push_output(tempdir_factory) + assert retc == 0 + assert 'Bash hook' in output + assert 'Passed' in output + + def test_pre_push_integration_empty_push(tempdir_factory): upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo') path = tempdir_factory.get() From 0a4fb173e40853c8d6ed2835748c1da48b961c29 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 24 Jan 2018 09:46:26 -0800 Subject: [PATCH 145/544] v1.5.1 --- CHANGELOG.md | 8 ++++++++ setup.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38a47120c..a894a4fdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +1.5.1 +===== + +### Fixes +- proper detection for root commit during pre-push + - #503 PR by @philipgian. + - #692 PR by @samskiter. + 1.5.0 ===== diff --git a/setup.py b/setup.py index 0a0ca2246..2210f8961 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.5.0', + version='1.5.1', author='Anthony Sottile', author_email='asottile@umich.edu', From 49dc689bf0b6d5c6f6903b970eb117318ec5d98f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 29 Jan 2018 21:47:35 -0800 Subject: [PATCH 146/544] Fix legacy commit-msg hooks --- pre_commit/resources/hook-tmpl | 2 +- tests/commands/install_uninstall_test.py | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index ded311cfa..b7f16231e 100644 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -27,7 +27,7 @@ else fi # Run the legacy pre-commit if it exists -if [ -x "$HERE"/{hook_type}.legacy ] && ! "$HERE"/{hook_type}.legacy; then +if [ -x "$HERE"/{hook_type}.legacy ] && ! "$HERE"/{hook_type}.legacy "$@"; then retv=1 fi diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 1659684a4..1469a3eea 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -611,6 +611,30 @@ def test_commit_msg_integration_passing(commit_msg_repo, tempdir_factory): assert first_line.endswith('...Passed') +def test_commit_msg_legacy(commit_msg_repo, tempdir_factory): + runner = Runner(commit_msg_repo, C.CONFIG_FILE) + + hook_path = runner.get_hook_path('commit-msg') + mkdirp(os.path.dirname(hook_path)) + with io.open(hook_path, 'w') as hook_file: + hook_file.write( + '#!/usr/bin/env bash\n' + 'set -eu\n' + 'test -e "$1"\n' + 'echo legacy\n', + ) + make_executable(hook_path) + + install(runner, hook_type='commit-msg') + + msg = 'Hi\nSigned off by: asottile' + retc, out = _get_commit_output(tempdir_factory, commit_msg=msg) + assert retc == 0 + first_line, second_line = out.splitlines()[:2] + assert first_line == 'legacy' + assert second_line.startswith('Must have "Signed off by:"...') + + def test_install_disallow_mising_config(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): From b319d6f80c7a3d92db8878289a7c6fd9fc408271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Wed, 31 Jan 2018 23:05:35 +0100 Subject: [PATCH 147/544] Add a hook option that allows stdout to be printed when exit code is 0 (#695) --- pre_commit/clientlib.py | 1 + pre_commit/commands/run.py | 5 ++++- tests/commands/run_test.py | 17 +++++++++++++++++ tests/repository_test.py | 1 + 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index c94691a58..cfe460f50 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -68,6 +68,7 @@ def _make_argparser(filenames_help): schema.Optional('log_file', schema.check_string, ''), schema.Optional('minimum_pre_commit_version', schema.check_string, '0'), schema.Optional('stages', schema.check_array(schema.check_string), []), + schema.Optional('verbose', schema.check_bool, False), ) MANIFEST_SCHEMA = schema.Array(MANIFEST_HOOK_DICT) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index a16d8fe11..98ae25dc8 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -130,7 +130,10 @@ def _run_single_hook(filenames, hook, repo, args, skips, cols): output.write_line(color.format_color(pass_fail, print_color, args.color)) - if (stdout or stderr or file_modifications) and (retcode or args.verbose): + if ( + (stdout or stderr or file_modifications) and + (retcode or args.verbose or hook['verbose']) + ): output.write_line('hookid: {}\n'.format(hook['id'])) # Print a message if failing due to file modifications diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index d800365fd..94dd52199 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -292,6 +292,23 @@ def test_always_run_alt_config( ) +def test_hook_verbose_enabled( + cap_out, repo_with_passing_hook, mock_out_store_directory, +): + with modify_config() as config: + config['repos'][0]['hooks'][0]['always_run'] = True + config['repos'][0]['hooks'][0]['verbose'] = True + + _test_run( + cap_out, + repo_with_passing_hook, + {}, + (b'Hello World',), + 0, + stage=False, + ) + + @pytest.mark.parametrize( ('origin', 'source', 'expect_failure'), ( diff --git a/tests/repository_test.py b/tests/repository_test.py index c160581e6..068d6bac7 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -789,4 +789,5 @@ def test_manifest_hooks(tempdir_factory, store): 'stages': [], 'types': ['file'], 'exclude_types': [], + 'verbose': False, } From 5eedbfc2d57cddcd64cd5d6615ef71592e57c38a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 1 Feb 2018 11:15:30 -0800 Subject: [PATCH 148/544] Change ignored cache dir for pytest 3.4.0 Committed via https://github.com/asottile/all-repos --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a18245731..ae552f4aa 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,4 @@ /venv* coverage-html dist -.cache +.pytest_cache From 5c90c1a68ffae6995b0a4201b4abfb938c45da49 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 3 Feb 2018 18:57:59 -0800 Subject: [PATCH 149/544] Rewrite the hook template in python --- pre_commit/commands/install_uninstall.py | 46 +++-- pre_commit/resources/commit-msg-tmpl | 1 - pre_commit/resources/hook-tmpl | 222 +++++++++++++++++------ pre_commit/resources/pre-push-tmpl | 31 ---- tests/commands/install_uninstall_test.py | 65 ++++--- 5 files changed, 223 insertions(+), 142 deletions(-) delete mode 100644 pre_commit/resources/commit-msg-tmpl mode change 100644 => 100755 pre_commit/resources/hook-tmpl delete mode 100644 pre_commit/resources/pre-push-tmpl diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 01aad52d6..83b97cb1e 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -3,7 +3,6 @@ import io import os.path -import pipes import sys from pre_commit import output @@ -21,6 +20,8 @@ 'e358c9dae00eac5d06b38dfdb1e33a8c', ) CURRENT_HASH = '138fd403232d2ddd5efb44317e38bf03' +TEMPLATE_START = '# start templated\n' +TEMPLATE_END = '# end templated\n' def is_our_script(filename): @@ -50,32 +51,27 @@ def install( elif os.path.exists(legacy_path): output.write_line( 'Running in migration mode with existing hooks at {}\n' - 'Use -f to use only pre-commit.'.format( - legacy_path, - ), + 'Use -f to use only pre-commit.'.format(legacy_path), ) - with io.open(hook_path, 'w') as pre_commit_file_obj: - if hook_type == 'pre-push': - with io.open(resource_filename('pre-push-tmpl')) as f: - hook_specific_contents = f.read() - elif hook_type == 'commit-msg': - with io.open(resource_filename('commit-msg-tmpl')) as f: - hook_specific_contents = f.read() - elif hook_type == 'pre-commit': - hook_specific_contents = '' - else: - raise AssertionError('Unknown hook type: {}'.format(hook_type)) - - skip_on_missing_conf = 'true' if skip_on_missing_conf else 'false' - contents = io.open(resource_filename('hook-tmpl')).read().format( - sys_executable=pipes.quote(sys.executable), - hook_type=hook_type, - hook_specific=hook_specific_contents, - config_file=runner.config_file, - skip_on_missing_conf=skip_on_missing_conf, - ) - pre_commit_file_obj.write(contents) + params = { + 'CONFIG': runner.config_file, + 'HOOK_TYPE': hook_type, + 'INSTALL_PYTHON': sys.executable, + 'SKIP_ON_MISSING_CONFIG': skip_on_missing_conf, + } + + with io.open(hook_path, 'w') as hook_file: + with io.open(resource_filename('hook-tmpl')) as f: + contents = f.read() + before, rest = contents.split(TEMPLATE_START) + to_template, after = rest.split(TEMPLATE_END) + + hook_file.write(before + TEMPLATE_START) + for line in to_template.splitlines(): + var = line.split()[0] + hook_file.write('{} = {!r}\n'.format(var, params[var])) + hook_file.write(TEMPLATE_END + after) make_executable(hook_path) output.write_line('pre-commit installed at {}'.format(hook_path)) diff --git a/pre_commit/resources/commit-msg-tmpl b/pre_commit/resources/commit-msg-tmpl deleted file mode 100644 index 182f214a5..000000000 --- a/pre_commit/resources/commit-msg-tmpl +++ /dev/null @@ -1 +0,0 @@ -args="--hook-stage=commit-msg --commit-msg-filename=$1" diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl old mode 100644 new mode 100755 index b7f16231e..2a9657ed2 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -1,55 +1,167 @@ -#!/usr/bin/env bash -# This is a randomish md5 to identify this script -# 138fd403232d2ddd5efb44317e38bf03 - -pushd "$(dirname "$0")" >& /dev/null -HERE="$(pwd)" -popd >& /dev/null - -retv=0 -args="" - -ENV_PYTHON={sys_executable} -SKIP_ON_MISSING_CONF={skip_on_missing_conf} - -if which pre-commit >& /dev/null; then - exe="pre-commit" - run_args="" -elif "$ENV_PYTHON" -c 'import pre_commit.main' >& /dev/null; then - exe="$ENV_PYTHON" - run_args="-m pre_commit.main" -elif python -c 'import pre_commit.main' >& /dev/null; then - exe="python" - run_args="-m pre_commit.main" -else - echo '`pre-commit` not found. Did you forget to activate your virtualenv?' - exit 1 -fi - -# Run the legacy pre-commit if it exists -if [ -x "$HERE"/{hook_type}.legacy ] && ! "$HERE"/{hook_type}.legacy "$@"; then - retv=1 -fi - -CONF_FILE="$(git rev-parse --show-toplevel)/{config_file}" -if [ ! -f "$CONF_FILE" ]; then - if [ "$SKIP_ON_MISSING_CONF" = true -o ! -z "$PRE_COMMIT_ALLOW_NO_CONFIG" ]; then - echo '`{config_file}` config file not found. Skipping `pre-commit`.' - exit $retv - else - echo 'No {config_file} file was found' - echo '- To temporarily silence this, run `PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`' - echo '- To permanently silence this, install pre-commit with the `--allow-missing-config` option' - echo '- To uninstall pre-commit run `pre-commit uninstall`' - exit 1 - fi -fi - -{hook_specific} - -# Run pre-commit -if ! "$exe" $run_args run $args --config {config_file}; then - retv=1 -fi - -exit $retv +#!/usr/bin/env python +"""File generated by pre-commit: https://pre-commit.com""" +from __future__ import print_function + +import distutils.spawn +import os +import subprocess +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +Z40 = '0' * 40 +ID_HASH = '138fd403232d2ddd5efb44317e38bf03' +# start templated +CONFIG = None +HOOK_TYPE = None +INSTALL_PYTHON = None +SKIP_ON_MISSING_CONFIG = None +# end templated + + +class EarlyExit(RuntimeError): + pass + + +class FatalError(RuntimeError): + pass + + +def _norm_exe(exe): + """Necessary for shebang support on windows. + + roughly lifted from `identify.identify.parse_shebang` + """ + with open(exe, 'rb') as f: + if f.read(2) != b'#!': + return () + try: + first_line = f.readline().decode('UTF-8') + except UnicodeDecodeError: + return () + + cmd = first_line.split() + if cmd[0] == '/usr/bin/env': + del cmd[0] + return tuple(cmd) + + +def _run_legacy(): + if HOOK_TYPE == 'pre-push': + stdin = getattr(sys.stdin, 'buffer', sys.stdin).read() + else: + stdin = None + + legacy_hook = os.path.join(HERE, '{}.legacy'.format(HOOK_TYPE)) + if os.access(legacy_hook, os.X_OK): + cmd = _norm_exe(legacy_hook) + (legacy_hook,) + tuple(sys.argv[1:]) + proc = subprocess.Popen(cmd, stdin=subprocess.PIPE if stdin else None) + proc.communicate(stdin) + return proc.returncode, stdin + else: + return 0, stdin + + +def _validate_config(): + cmd = ('git', 'rev-parse', '--show-toplevel') + top_level = subprocess.check_output(cmd).decode('UTF-8').strip() + cfg = os.path.join(top_level, CONFIG) + if os.path.isfile(cfg): + pass + elif SKIP_ON_MISSING_CONFIG or os.getenv('PRE_COMMIT_ALLOW_NO_CONFIG'): + print( + '`{}` config file not found. ' + 'Skipping `pre-commit`.'.format(CONFIG), + ) + raise EarlyExit() + else: + raise FatalError( + 'No {} file was found\n' + '- To temporarily silence this, run ' + '`PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`\n' + '- To permanently silence this, install pre-commit with the ' + '--allow-missing-config option\n' + '- To uninstall pre-commit run ' + '`pre-commit uninstall`'.format(CONFIG), + ) + + +def _exe(): + with open(os.devnull, 'wb') as devnull: + for exe in (INSTALL_PYTHON, sys.executable): + try: + if not subprocess.call( + (exe, '-c', 'import pre_commit.main'), + stdout=devnull, stderr=devnull, + ): + return (exe, '-m', 'pre_commit.main', 'run') + except OSError: + pass + + if distutils.spawn.find_executable('pre-commit'): + return ('pre-commit', 'run') + + raise FatalError( + '`pre-commit` not found. Did you forget to activate your virtualenv?', + ) + + +def _pre_push(stdin): + remote = sys.argv[1] + + opts = () + for line in stdin.decode('UTF-8').splitlines(): + _, local_sha, _, remote_sha = line.split() + if local_sha == Z40: + continue + elif remote_sha != Z40: + opts = ('--origin', local_sha, '--source', remote_sha) + else: + # First ancestor not found in remote + first_ancestor = subprocess.check_output(( + 'git', 'rev-list', '--max-count=1', '--topo-order', + '--reverse', local_sha, '--not', '--remotes={}'.format(remote), + )).decode().strip() + if not first_ancestor: + continue + else: + cmd = ('git', 'rev-list', '--max-parents=0', local_sha) + roots = set(subprocess.check_output(cmd).decode().splitlines()) + if first_ancestor in roots: + # pushing the whole tree including root commit + opts = ('--all-files',) + else: + cmd = ('git', 'rev-parse', '{}^'.format(first_ancestor)) + source = subprocess.check_output(cmd).decode().strip() + opts = ('--origin', local_sha, '--source', source) + + if opts: + return opts + else: + # An attempt to push an empty changeset + raise EarlyExit() + + +def _opts(stdin): + fns = { + 'commit-msg': lambda _: ('--commit-msg-filename', sys.argv[1]), + 'pre-commit': lambda _: (), + 'pre-push': _pre_push, + } + stage = HOOK_TYPE.replace('pre-', '') + return ('--config', CONFIG, '--hook-stage', stage) + fns[HOOK_TYPE](stdin) + + +def main(): + retv, stdin = _run_legacy() + try: + _validate_config() + return retv | subprocess.call(_exe() + _opts(stdin)) + except EarlyExit: + return retv + except FatalError as e: + print(e.args[0]) + return 1 + + +if __name__ == '__main__': + exit(main()) diff --git a/pre_commit/resources/pre-push-tmpl b/pre_commit/resources/pre-push-tmpl deleted file mode 100644 index 0a3dad572..000000000 --- a/pre_commit/resources/pre-push-tmpl +++ /dev/null @@ -1,31 +0,0 @@ -z40=0000000000000000000000000000000000000000 -while read local_ref local_sha remote_ref remote_sha -do - if [ "$local_sha" != $z40 ]; then - if [ "$remote_sha" = $z40 ]; then - # First ancestor not found in remote - first_ancestor=$(git rev-list --topo-order --reverse "$local_sha" --not --remotes="$1" | head -n 1) - if [ -n "$first_ancestor" ]; then - # Check that the ancestor has at least one parent - git rev-list --max-parents=0 "$local_sha" | grep "$first_ancestor" > /dev/null - if [ $? -eq 0 ]; then - # Pushing the whole tree, including the root commit, so run on all files - args="--all-files" - else - source=$(git rev-parse "$first_ancestor"^) - args="--origin $local_sha --source $source" - fi - fi - else - args="--origin $local_sha --source $remote_sha" - fi - fi -done - -if [ "$args" != "" ]; then - args="$args --hook-stage push" -else - # If args is empty, then an attempt to push on an empty - # changeset is being made. In this case, just exit cleanly - exit 0 -fi diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 1469a3eea..ea6727e41 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -4,7 +4,6 @@ import io import os.path -import pipes import re import shutil import subprocess @@ -49,35 +48,11 @@ def test_is_previous_pre_commit(tmpdir): def test_install_pre_commit(tempdir_factory): path = git_dir(tempdir_factory) runner = Runner(path, C.CONFIG_FILE) - ret = install(runner) - assert ret == 0 - assert os.path.exists(runner.pre_commit_path) - pre_commit_contents = io.open(runner.pre_commit_path).read() - pre_commit_script = resource_filename('hook-tmpl') - expected_contents = io.open(pre_commit_script).read().format( - sys_executable=pipes.quote(sys.executable), - hook_type='pre-commit', - hook_specific='', - config_file=runner.config_file, - skip_on_missing_conf='false', - ) - assert pre_commit_contents == expected_contents + assert not install(runner) assert os.access(runner.pre_commit_path, os.X_OK) - ret = install(runner, hook_type='pre-push') - assert ret == 0 - assert os.path.exists(runner.pre_push_path) - pre_push_contents = io.open(runner.pre_push_path).read() - pre_push_tmpl = resource_filename('pre-push-tmpl') - pre_push_template_contents = io.open(pre_push_tmpl).read() - expected_contents = io.open(pre_commit_script).read().format( - sys_executable=pipes.quote(sys.executable), - hook_type='pre-push', - hook_specific=pre_push_template_contents, - config_file=runner.config_file, - skip_on_missing_conf='false', - ) - assert pre_push_contents == expected_contents + assert not install(runner, hook_type='pre-push') + assert os.access(runner.pre_push_path, os.X_OK) def test_install_hooks_directory_not_present(tempdir_factory): @@ -242,7 +217,7 @@ def test_environment_not_sourced(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): # Patch the executable to simulate rming virtualenv - with mock.patch.object(sys, 'executable', '/bin/false'): + with mock.patch.object(sys, 'executable', '/does-not-exist'): assert install(Runner(path, C.CONFIG_FILE)) == 0 # Use a specific homedir to ignore --user installs @@ -262,7 +237,7 @@ def test_environment_not_sourced(tempdir_factory): ) assert ret == 1 assert stdout == '' - assert stderr == ( + assert stderr.replace('\r\n', '\n') == ( '`pre-commit` not found. ' 'Did you forget to activate your virtualenv?\n' ) @@ -593,6 +568,36 @@ def test_pre_push_integration_empty_push(tempdir_factory): assert retc == 0 +def test_pre_push_legacy(tempdir_factory): + upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + path = tempdir_factory.get() + cmd_output('git', 'clone', upstream, path) + with cwd(path): + runner = Runner(path, C.CONFIG_FILE) + + hook_path = runner.get_hook_path('pre-push') + mkdirp(os.path.dirname(hook_path)) + with io.open(hook_path, 'w') as hook_file: + hook_file.write( + '#!/usr/bin/env bash\n' + 'set -eu\n' + 'read lr ls rr rs\n' + 'test -n "$lr" -a -n "$ls" -a -n "$rr" -a -n "$rs"\n' + 'echo legacy\n', + ) + make_executable(hook_path) + + install(runner, hook_type='pre-push') + assert _get_commit_output(tempdir_factory)[0] == 0 + + retc, output = _get_push_output(tempdir_factory) + assert retc == 0 + first_line, _, third_line = output.splitlines()[:3] + assert first_line == 'legacy' + assert third_line.startswith('Bash hook') + assert third_line.endswith('Passed') + + def test_commit_msg_integration_failing(commit_msg_repo, tempdir_factory): install(Runner(commit_msg_repo, C.CONFIG_FILE), hook_type='commit-msg') retc, out = _get_commit_output(tempdir_factory) From 8bb4d63d3b7c2a6494cd14ef8f8b9d96e103a953 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 4 Feb 2018 12:25:52 -0800 Subject: [PATCH 150/544] v1.6.0 --- CHANGELOG.md | 16 ++++++++++++++++ setup.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a894a4fdc..a60566859 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +1.6.0 +===== + +### Features +- Hooks now may have a `verbose` option to produce output even without failure + - #689 issue by @bagerard. + - #695 PR by @bagerard. +- Installed hook no longer requires `bash` + - #699 PR by @asottile. + +### Fixes +- legacy pre-push / commit-msg hooks are now invoked as if `git` called them + - #693 issue by @samskiter. + - #694 PR by @asottile. + - #699 PR by @asottile. + 1.5.1 ===== diff --git a/setup.py b/setup.py index 2210f8961..1637386cf 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.5.1', + version='1.6.0', author='Anthony Sottile', author_email='asottile@umich.edu', From bdad930d712d5461ca4d83bf7e8e8e27d4667cc3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 6 Feb 2018 23:14:47 -0800 Subject: [PATCH 151/544] Move pre_commit.schema to cfgv library --- pre_commit/clientlib.py | 92 ++-- pre_commit/commands/autoupdate.py | 2 +- .../meta_hooks/check_useless_excludes.py | 3 +- pre_commit/repository.py | 4 +- pre_commit/schema.py | 291 ------------ setup.py | 1 + testing/fixtures.py | 4 +- tests/clientlib_test.py | 30 +- tests/schema_test.py | 422 ------------------ 9 files changed, 58 insertions(+), 791 deletions(-) delete mode 100644 pre_commit/schema.py delete mode 100644 tests/schema_test.py diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index cfe460f50..bb772341a 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -5,25 +5,18 @@ import collections import functools +import cfgv from aspy.yaml import ordered_load from identify.identify import ALL_TAGS import pre_commit.constants as C -from pre_commit import schema from pre_commit.error_handler import FatalError from pre_commit.languages.all import all_languages -def check_language(v): - if v not in all_languages: - raise schema.ValidationError( - 'Expected {} to be in {!r}'.format(v, all_languages), - ) - - def check_type_tag(tag): if tag not in ALL_TAGS: - raise schema.ValidationError( + raise cfgv.ValidationError( 'Type tag {!r} is not recognized. ' 'Try upgrading identify and pre-commit?'.format(tag), ) @@ -36,41 +29,40 @@ def _make_argparser(filenames_help): return parser -MANIFEST_HOOK_DICT = schema.Map( +MANIFEST_HOOK_DICT = cfgv.Map( 'Hook', 'id', - schema.Required('id', schema.check_string), - schema.Required('name', schema.check_string), - schema.Required('entry', schema.check_string), - schema.Required( - 'language', schema.check_and(schema.check_string, check_language), + cfgv.Required('id', cfgv.check_string), + cfgv.Required('name', cfgv.check_string), + cfgv.Required('entry', cfgv.check_string), + cfgv.Required( + 'language', + cfgv.check_and(cfgv.check_string, cfgv.check_one_of(all_languages)), ), - schema.Optional( - 'files', schema.check_and(schema.check_string, schema.check_regex), - '', + cfgv.Optional( + 'files', cfgv.check_and(cfgv.check_string, cfgv.check_regex), '', ), - schema.Optional( - 'exclude', schema.check_and(schema.check_string, schema.check_regex), - '^$', + cfgv.Optional( + 'exclude', cfgv.check_and(cfgv.check_string, cfgv.check_regex), '^$', ), - schema.Optional('types', schema.check_array(check_type_tag), ['file']), - schema.Optional('exclude_types', schema.check_array(check_type_tag), []), + cfgv.Optional('types', cfgv.check_array(check_type_tag), ['file']), + cfgv.Optional('exclude_types', cfgv.check_array(check_type_tag), []), - schema.Optional( - 'additional_dependencies', schema.check_array(schema.check_string), [], + cfgv.Optional( + 'additional_dependencies', cfgv.check_array(cfgv.check_string), [], ), - schema.Optional('args', schema.check_array(schema.check_string), []), - schema.Optional('always_run', schema.check_bool, False), - schema.Optional('pass_filenames', schema.check_bool, True), - schema.Optional('description', schema.check_string, ''), - schema.Optional('language_version', schema.check_string, 'default'), - schema.Optional('log_file', schema.check_string, ''), - schema.Optional('minimum_pre_commit_version', schema.check_string, '0'), - schema.Optional('stages', schema.check_array(schema.check_string), []), - schema.Optional('verbose', schema.check_bool, False), + cfgv.Optional('args', cfgv.check_array(cfgv.check_string), []), + cfgv.Optional('always_run', cfgv.check_bool, False), + cfgv.Optional('pass_filenames', cfgv.check_bool, True), + cfgv.Optional('description', cfgv.check_string, ''), + cfgv.Optional('language_version', cfgv.check_string, 'default'), + cfgv.Optional('log_file', cfgv.check_string, ''), + cfgv.Optional('minimum_pre_commit_version', cfgv.check_string, '0'), + cfgv.Optional('stages', cfgv.check_array(cfgv.check_string), []), + cfgv.Optional('verbose', cfgv.check_bool, False), ) -MANIFEST_SCHEMA = schema.Array(MANIFEST_HOOK_DICT) +MANIFEST_SCHEMA = cfgv.Array(MANIFEST_HOOK_DICT) class InvalidManifestError(FatalError): @@ -78,7 +70,7 @@ class InvalidManifestError(FatalError): load_manifest = functools.partial( - schema.load_from_filename, + cfgv.load_from_filename, schema=MANIFEST_SCHEMA, load_strategy=ordered_load, exc_tp=InvalidManifestError, @@ -101,40 +93,40 @@ def validate_manifest_main(argv=None): _LOCAL_SENTINEL = 'local' _META_SENTINEL = 'meta' -CONFIG_HOOK_DICT = schema.Map( +CONFIG_HOOK_DICT = cfgv.Map( 'Hook', 'id', - schema.Required('id', schema.check_string), + cfgv.Required('id', cfgv.check_string), # All keys in manifest hook dict are valid in a config hook dict, but # are optional. # No defaults are provided here as the config is merged on top of the # manifest. *[ - schema.OptionalNoDefault(item.key, item.check_fn) + cfgv.OptionalNoDefault(item.key, item.check_fn) for item in MANIFEST_HOOK_DICT.items if item.key != 'id' ] ) -CONFIG_REPO_DICT = schema.Map( +CONFIG_REPO_DICT = cfgv.Map( 'Repository', 'repo', - schema.Required('repo', schema.check_string), - schema.RequiredRecurse('hooks', schema.Array(CONFIG_HOOK_DICT)), + cfgv.Required('repo', cfgv.check_string), + cfgv.RequiredRecurse('hooks', cfgv.Array(CONFIG_HOOK_DICT)), - schema.Conditional( - 'sha', schema.check_string, + cfgv.Conditional( + 'sha', cfgv.check_string, condition_key='repo', - condition_value=schema.NotIn(_LOCAL_SENTINEL, _META_SENTINEL), + condition_value=cfgv.NotIn(_LOCAL_SENTINEL, _META_SENTINEL), ensure_absent=True, ), ) -CONFIG_SCHEMA = schema.Map( +CONFIG_SCHEMA = cfgv.Map( 'Config', None, - schema.RequiredRecurse('repos', schema.Array(CONFIG_REPO_DICT)), - schema.Optional('exclude', schema.check_regex, '^$'), - schema.Optional('fail_fast', schema.check_bool, False), + cfgv.RequiredRecurse('repos', cfgv.Array(CONFIG_REPO_DICT)), + cfgv.Optional('exclude', cfgv.check_regex, '^$'), + cfgv.Optional('fail_fast', cfgv.check_bool, False), ) @@ -160,7 +152,7 @@ def ordered_load_normalize_legacy_config(contents): load_config = functools.partial( - schema.load_from_filename, + cfgv.load_from_filename, schema=CONFIG_SCHEMA, load_strategy=ordered_load_normalize_legacy_config, exc_tp=InvalidConfigError, diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 5ba5a8eba..ca83a5880 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -6,6 +6,7 @@ from aspy.yaml import ordered_dump from aspy.yaml import ordered_load +from cfgv import remove_defaults import pre_commit.constants as C from pre_commit import output @@ -15,7 +16,6 @@ from pre_commit.clientlib import load_config from pre_commit.commands.migrate_config import migrate_config from pre_commit.repository import Repository -from pre_commit.schema import remove_defaults from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cwd diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 189633a8a..cdc556df7 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -3,11 +3,12 @@ import argparse import re +from cfgv import apply_defaults + import pre_commit.constants as C from pre_commit import git from pre_commit.clientlib import load_config from pre_commit.clientlib import MANIFEST_HOOK_DICT -from pre_commit.schema import apply_defaults def exclude_matches_any(filenames, include, exclude): diff --git a/pre_commit/repository.py b/pre_commit/repository.py index e01b3d1d5..3ed160af5 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -11,6 +11,8 @@ import pkg_resources from cached_property import cached_property +from cfgv import apply_defaults +from cfgv import validate import pre_commit.constants as C from pre_commit import five @@ -22,8 +24,6 @@ from pre_commit.languages.all import languages from pre_commit.languages.helpers import environment_dir from pre_commit.prefix import Prefix -from pre_commit.schema import apply_defaults -from pre_commit.schema import validate logger = logging.getLogger('pre_commit') diff --git a/pre_commit/schema.py b/pre_commit/schema.py deleted file mode 100644 index 89e1bcfc5..000000000 --- a/pre_commit/schema.py +++ /dev/null @@ -1,291 +0,0 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - -import collections -import contextlib -import io -import os.path -import re -import sys - -import six - - -class ValidationError(ValueError): - def __init__(self, error_msg, ctx=None): - super(ValidationError, self).__init__(error_msg) - self.error_msg = error_msg - self.ctx = ctx - - def __str__(self): - out = '\n' - err = self - while err.ctx is not None: - out += '==> {}\n'.format(err.ctx) - err = err.error_msg - out += '=====> {}'.format(err.error_msg) - return out - - -MISSING = collections.namedtuple('Missing', ())() -type(MISSING).__repr__ = lambda self: 'MISSING' - - -@contextlib.contextmanager -def validate_context(msg): - try: - yield - except ValidationError as e: - _, _, tb = sys.exc_info() - six.reraise(ValidationError, ValidationError(e, ctx=msg), tb) - - -@contextlib.contextmanager -def reraise_as(tp): - try: - yield - except ValidationError as e: - _, _, tb = sys.exc_info() - six.reraise(tp, tp(e), tb) - - -def _dct_noop(self, dct): - pass - - -def _check_optional(self, dct): - if self.key not in dct: - return - with validate_context('At key: {}'.format(self.key)): - self.check_fn(dct[self.key]) - - -def _apply_default_optional(self, dct): - dct.setdefault(self.key, self.default) - - -def _remove_default_optional(self, dct): - if dct.get(self.key, MISSING) == self.default: - del dct[self.key] - - -def _require_key(self, dct): - if self.key not in dct: - raise ValidationError('Missing required key: {}'.format(self.key)) - - -def _check_required(self, dct): - _require_key(self, dct) - _check_optional(self, dct) - - -@property -def _check_fn_required_recurse(self): - def check_fn(val): - validate(val, self.schema) - return check_fn - - -def _apply_default_required_recurse(self, dct): - dct[self.key] = apply_defaults(dct[self.key], self.schema) - - -def _remove_default_required_recurse(self, dct): - dct[self.key] = remove_defaults(dct[self.key], self.schema) - - -def _check_conditional(self, dct): - if dct.get(self.condition_key, MISSING) == self.condition_value: - _check_required(self, dct) - elif self.condition_key in dct and self.ensure_absent and self.key in dct: - if isinstance(self.condition_value, Not): - op = 'is' - cond_val = self.condition_value.val - elif isinstance(self.condition_value, NotIn): - op = 'is any of' - cond_val = self.condition_value.values - else: - op = 'is not' - cond_val = self.condition_value - raise ValidationError( - 'Expected {key} to be absent when {cond_key} {op} {cond_val!r}, ' - 'found {key}: {val!r}'.format( - key=self.key, - val=dct[self.key], - cond_key=self.condition_key, - op=op, - cond_val=cond_val, - ), - ) - - -Required = collections.namedtuple('Required', ('key', 'check_fn')) -Required.check = _check_required -Required.apply_default = _dct_noop -Required.remove_default = _dct_noop -RequiredRecurse = collections.namedtuple('RequiredRecurse', ('key', 'schema')) -RequiredRecurse.check = _check_required -RequiredRecurse.check_fn = _check_fn_required_recurse -RequiredRecurse.apply_default = _apply_default_required_recurse -RequiredRecurse.remove_default = _remove_default_required_recurse -Optional = collections.namedtuple('Optional', ('key', 'check_fn', 'default')) -Optional.check = _check_optional -Optional.apply_default = _apply_default_optional -Optional.remove_default = _remove_default_optional -OptionalNoDefault = collections.namedtuple( - 'OptionalNoDefault', ('key', 'check_fn'), -) -OptionalNoDefault.check = _check_optional -OptionalNoDefault.apply_default = _dct_noop -OptionalNoDefault.remove_default = _dct_noop -Conditional = collections.namedtuple( - 'Conditional', - ('key', 'check_fn', 'condition_key', 'condition_value', 'ensure_absent'), -) -Conditional.__new__.__defaults__ = (False,) -Conditional.check = _check_conditional -Conditional.apply_default = _dct_noop -Conditional.remove_default = _dct_noop - - -class Map(collections.namedtuple('Map', ('object_name', 'id_key', 'items'))): - __slots__ = () - - def __new__(cls, object_name, id_key, *items): - return super(Map, cls).__new__(cls, object_name, id_key, items) - - def check(self, v): - if not isinstance(v, dict): - raise ValidationError('Expected a {} map but got a {}'.format( - self.object_name, type(v).__name__, - )) - if self.id_key is None: - context = 'At {}()'.format(self.object_name) - else: - context = 'At {}({}={!r})'.format( - self.object_name, self.id_key, v.get(self.id_key, MISSING), - ) - with validate_context(context): - for item in self.items: - item.check(v) - - def apply_defaults(self, v): - ret = v.copy() - for item in self.items: - item.apply_default(ret) - return ret - - def remove_defaults(self, v): - ret = v.copy() - for item in self.items: - item.remove_default(ret) - return ret - - -class Array(collections.namedtuple('Array', ('of',))): - __slots__ = () - - def check(self, v): - check_array(check_any)(v) - if not v: - raise ValidationError( - "Expected at least 1 '{}'".format(self.of.object_name), - ) - for val in v: - validate(val, self.of) - - def apply_defaults(self, v): - return [apply_defaults(val, self.of) for val in v] - - def remove_defaults(self, v): - return [remove_defaults(val, self.of) for val in v] - - -class Not(collections.namedtuple('Not', ('val',))): - def __eq__(self, other): - return other is not MISSING and other != self.val - - -class NotIn(collections.namedtuple('NotIn', ('values',))): - def __new__(cls, *values): - return super(NotIn, cls).__new__(cls, values=values) - - def __eq__(self, other): - return other is not MISSING and other not in self.values - - -def check_any(_): - pass - - -def check_type(tp, typename=None): - def check_type_fn(v): - if not isinstance(v, tp): - raise ValidationError( - 'Expected {} got {}'.format( - typename or tp.__name__, type(v).__name__, - ), - ) - return check_type_fn - - -check_bool = check_type(bool) -check_string = check_type(six.string_types, typename='string') - - -def check_regex(v): - try: - re.compile(v) - except re.error: - raise ValidationError('{!r} is not a valid python regex'.format(v)) - - -def check_array(inner_check): - def check_array_fn(v): - if not isinstance(v, (list, tuple)): - raise ValidationError( - 'Expected array but got {!r}'.format(type(v).__name__), - ) - - for i, val in enumerate(v): - with validate_context('At index {}'.format(i)): - inner_check(val) - return check_array_fn - - -def check_and(*fns): - def check(v): - for fn in fns: - fn(v) - return check - - -def validate(v, schema): - schema.check(v) - return v - - -def apply_defaults(v, schema): - return schema.apply_defaults(v) - - -def remove_defaults(v, schema): - return schema.remove_defaults(v) - - -def load_from_filename(filename, schema, load_strategy, exc_tp): - with reraise_as(exc_tp): - if not os.path.exists(filename): - raise ValidationError('{} does not exist'.format(filename)) - - with io.open(filename) as f: - contents = f.read() - - with validate_context('File {}'.format(filename)): - try: - data = load_strategy(contents) - except Exception as e: - raise ValidationError(str(e)) - - validate(data, schema) - return apply_defaults(data, schema) diff --git a/setup.py b/setup.py index 1637386cf..99c5f44d9 100644 --- a/setup.py +++ b/setup.py @@ -37,6 +37,7 @@ install_requires=[ 'aspy.yaml', 'cached-property', + 'cfgv>=1.0.0', 'identify>=1.0.0', 'nodeenv>=0.11.1', 'pyyaml', diff --git a/testing/fixtures.py b/testing/fixtures.py index edb1bcdf4..bff32805d 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -8,13 +8,13 @@ from aspy.yaml import ordered_dump from aspy.yaml import ordered_load +from cfgv import apply_defaults +from cfgv import validate import pre_commit.constants as C from pre_commit import git from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest -from pre_commit.schema import apply_defaults -from pre_commit.schema import validate from pre_commit.util import cmd_output from pre_commit.util import copy_tree_to_path from pre_commit.util import cwd diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 8e85e6c44..2f0b6fcb3 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -1,9 +1,8 @@ from __future__ import unicode_literals +import cfgv import pytest -from pre_commit import schema -from pre_commit.clientlib import check_language from pre_commit.clientlib import check_type_tag from pre_commit.clientlib import CONFIG_HOOK_DICT from pre_commit.clientlib import CONFIG_SCHEMA @@ -16,29 +15,18 @@ def is_valid_according_to_schema(obj, obj_schema): try: - schema.validate(obj, obj_schema) + cfgv.validate(obj, obj_schema) return True - except schema.ValidationError: + except cfgv.ValidationError: return False -@pytest.mark.parametrize('value', ('not a language', 'python3')) -def test_check_language_failures(value): - with pytest.raises(schema.ValidationError): - check_language(value) - - @pytest.mark.parametrize('value', ('definitely-not-a-tag', 'fiel')) def test_check_type_tag_failures(value): - with pytest.raises(schema.ValidationError): + with pytest.raises(cfgv.ValidationError): check_type_tag(value) -@pytest.mark.parametrize('value', ('python', 'node', 'pcre')) -def test_check_language_ok(value): - check_language(value) - - def test_is_local_repo(): assert is_local_repo({'repo': 'local'}) @@ -58,7 +46,6 @@ def test_validate_config_main(args, expected_output): @pytest.mark.parametrize( ('config_obj', 'expected'), ( - ([], False), ( {'repos': [{ 'repo': 'git@github.com:pre-commit/pre-commit-hooks', @@ -116,8 +103,8 @@ def test_config_with_local_hooks_definition_fails(): 'files': '^(.*)$', }], }]} - with pytest.raises(schema.ValidationError): - schema.validate(config_obj, CONFIG_SCHEMA) + with pytest.raises(cfgv.ValidationError): + cfgv.validate(config_obj, CONFIG_SCHEMA) @pytest.mark.parametrize( @@ -147,7 +134,7 @@ def test_config_with_local_hooks_definition_fails(): ), ) def test_config_with_local_hooks_definition_passes(config_obj): - schema.validate(config_obj, CONFIG_SCHEMA) + cfgv.validate(config_obj, CONFIG_SCHEMA) def test_config_schema_does_not_contain_defaults(): @@ -155,7 +142,7 @@ def test_config_schema_does_not_contain_defaults(): will clobber potentially useful values in the backing manifest. #227 """ for item in CONFIG_HOOK_DICT.items: - assert not isinstance(item, schema.Optional) + assert not isinstance(item, cfgv.Optional) @pytest.mark.parametrize( @@ -174,7 +161,6 @@ def test_validate_manifest_main(args, expected_output): @pytest.mark.parametrize( ('manifest_obj', 'expected'), ( - ([], False), ( [{ 'id': 'a', diff --git a/tests/schema_test.py b/tests/schema_test.py deleted file mode 100644 index 565f7e174..000000000 --- a/tests/schema_test.py +++ /dev/null @@ -1,422 +0,0 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - -import json - -import mock -import pytest - -from pre_commit.schema import apply_defaults -from pre_commit.schema import Array -from pre_commit.schema import check_and -from pre_commit.schema import check_any -from pre_commit.schema import check_array -from pre_commit.schema import check_bool -from pre_commit.schema import check_regex -from pre_commit.schema import check_type -from pre_commit.schema import Conditional -from pre_commit.schema import load_from_filename -from pre_commit.schema import Map -from pre_commit.schema import MISSING -from pre_commit.schema import Not -from pre_commit.schema import NotIn -from pre_commit.schema import Optional -from pre_commit.schema import OptionalNoDefault -from pre_commit.schema import remove_defaults -from pre_commit.schema import Required -from pre_commit.schema import RequiredRecurse -from pre_commit.schema import validate -from pre_commit.schema import ValidationError - - -def _assert_exception_trace(e, trace): - inner = e - for ctx in trace[:-1]: - assert inner.ctx == ctx - inner = inner.error_msg - assert inner.error_msg == trace[-1] - - -def test_ValidationError_simple_str(): - assert str(ValidationError('error msg')) == ( - '\n' - '=====> error msg' - ) - - -def test_ValidationError_nested(): - error = ValidationError( - ValidationError( - ValidationError('error msg'), - ctx='At line 1', - ), - ctx='In file foo', - ) - assert str(error) == ( - '\n' - '==> In file foo\n' - '==> At line 1\n' - '=====> error msg' - ) - - -def test_check_regex(): - with pytest.raises(ValidationError) as excinfo: - check_regex(str('(')) - assert excinfo.value.error_msg == "'(' is not a valid python regex" - - -def test_check_regex_ok(): - check_regex('^$') - - -def test_check_array_failed_inner_check(): - check = check_array(check_bool) - with pytest.raises(ValidationError) as excinfo: - check([True, False, 5]) - _assert_exception_trace( - excinfo.value, ('At index 2', 'Expected bool got int'), - ) - - -def test_check_array_ok(): - check_array(check_bool)([True, False]) - - -def test_check_and(): - check = check_and(check_type(str), check_regex) - with pytest.raises(ValidationError) as excinfo: - check(True) - assert excinfo.value.error_msg == 'Expected str got bool' - with pytest.raises(ValidationError) as excinfo: - check(str('(')) - assert excinfo.value.error_msg == "'(' is not a valid python regex" - - -def test_check_and_ok(): - check = check_and(check_type(str), check_regex) - check(str('^$')) - - -@pytest.mark.parametrize( - ('val', 'expected'), - (('bar', True), ('foo', False), (MISSING, False)), -) -def test_not(val, expected): - compared = Not('foo') - assert (val == compared) is expected - assert (compared == val) is expected - - -@pytest.mark.parametrize( - ('values', 'expected'), - (('bar', True), ('foo', False), (MISSING, False)), -) -def test_not_in(values, expected): - compared = NotIn('baz', 'foo') - assert (values == compared) is expected - assert (compared == values) is expected - - -trivial_array_schema = Array(Map('foo', 'id')) - - -def test_validate_top_level_array_not_an_array(): - with pytest.raises(ValidationError) as excinfo: - validate({}, trivial_array_schema) - assert excinfo.value.error_msg == "Expected array but got 'dict'" - - -def test_validate_top_level_array_no_objects(): - with pytest.raises(ValidationError) as excinfo: - validate([], trivial_array_schema) - assert excinfo.value.error_msg == "Expected at least 1 'foo'" - - -@pytest.mark.parametrize('v', (({},), [{}])) -def test_ok_both_types(v): - validate(v, trivial_array_schema) - - -map_required = Map('foo', 'key', Required('key', check_bool)) -map_optional = Map('foo', 'key', Optional('key', check_bool, False)) -map_no_default = Map('foo', 'key', OptionalNoDefault('key', check_bool)) - - -def test_map_wrong_type(): - with pytest.raises(ValidationError) as excinfo: - validate([], map_required) - assert excinfo.value.error_msg == 'Expected a foo map but got a list' - - -def test_required_missing_key(): - with pytest.raises(ValidationError) as excinfo: - validate({}, map_required) - _assert_exception_trace( - excinfo.value, ('At foo(key=MISSING)', 'Missing required key: key'), - ) - - -@pytest.mark.parametrize( - 'schema', (map_required, map_optional, map_no_default), -) -def test_map_value_wrong_type(schema): - with pytest.raises(ValidationError) as excinfo: - validate({'key': 5}, schema) - _assert_exception_trace( - excinfo.value, - ('At foo(key=5)', 'At key: key', 'Expected bool got int'), - ) - - -@pytest.mark.parametrize( - 'schema', (map_required, map_optional, map_no_default), -) -def test_map_value_correct_type(schema): - validate({'key': True}, schema) - - -@pytest.mark.parametrize('schema', (map_optional, map_no_default)) -def test_optional_key_missing(schema): - validate({}, schema) - - -map_conditional = Map( - 'foo', 'key', - Conditional( - 'key2', check_bool, condition_key='key', condition_value=True, - ), -) -map_conditional_not = Map( - 'foo', 'key', - Conditional( - 'key2', check_bool, condition_key='key', condition_value=Not(False), - ), -) -map_conditional_absent = Map( - 'foo', 'key', - Conditional( - 'key2', check_bool, - condition_key='key', condition_value=True, ensure_absent=True, - ), -) -map_conditional_absent_not = Map( - 'foo', 'key', - Conditional( - 'key2', check_bool, - condition_key='key', condition_value=Not(True), ensure_absent=True, - ), -) -map_conditional_absent_not_in = Map( - 'foo', 'key', - Conditional( - 'key2', check_bool, - condition_key='key', condition_value=NotIn(1, 2), ensure_absent=True, - ), -) - - -@pytest.mark.parametrize('schema', (map_conditional, map_conditional_not)) -@pytest.mark.parametrize( - 'v', - ( - # Conditional check passes, key2 is checked and passes - {'key': True, 'key2': True}, - # Conditional check fails, key2 is not checked - {'key': False, 'key2': 'ohai'}, - ), -) -def test_ok_conditional_schemas(v, schema): - validate(v, schema) - - -@pytest.mark.parametrize('schema', (map_conditional, map_conditional_not)) -def test_not_ok_conditional_schemas(schema): - with pytest.raises(ValidationError) as excinfo: - validate({'key': True, 'key2': 5}, schema) - _assert_exception_trace( - excinfo.value, - ('At foo(key=True)', 'At key: key2', 'Expected bool got int'), - ) - - -def test_ensure_absent_conditional(): - with pytest.raises(ValidationError) as excinfo: - validate({'key': False, 'key2': True}, map_conditional_absent) - _assert_exception_trace( - excinfo.value, - ( - 'At foo(key=False)', - 'Expected key2 to be absent when key is not True, ' - 'found key2: True', - ), - ) - - -def test_ensure_absent_conditional_not(): - with pytest.raises(ValidationError) as excinfo: - validate({'key': True, 'key2': True}, map_conditional_absent_not) - _assert_exception_trace( - excinfo.value, - ( - 'At foo(key=True)', - 'Expected key2 to be absent when key is True, ' - 'found key2: True', - ), - ) - - -def test_ensure_absent_conditional_not_in(): - with pytest.raises(ValidationError) as excinfo: - validate({'key': 1, 'key2': True}, map_conditional_absent_not_in) - _assert_exception_trace( - excinfo.value, - ( - 'At foo(key=1)', - 'Expected key2 to be absent when key is any of (1, 2), ' - 'found key2: True', - ), - ) - - -def test_no_error_conditional_absent(): - validate({}, map_conditional_absent) - validate({}, map_conditional_absent_not) - validate({'key2': True}, map_conditional_absent) - validate({'key2': True}, map_conditional_absent_not) - - -def test_apply_defaults_copies_object(): - val = {} - ret = apply_defaults(val, map_optional) - assert ret is not val - - -def test_apply_defaults_sets_default(): - ret = apply_defaults({}, map_optional) - assert ret == {'key': False} - - -def test_apply_defaults_does_not_change_non_default(): - ret = apply_defaults({'key': True}, map_optional) - assert ret == {'key': True} - - -def test_apply_defaults_does_nothing_on_non_optional(): - ret = apply_defaults({}, map_required) - assert ret == {} - - -def test_apply_defaults_map_in_list(): - ret = apply_defaults([{}], Array(map_optional)) - assert ret == [{'key': False}] - - -def test_remove_defaults_copies_object(): - val = {'key': False} - ret = remove_defaults(val, map_optional) - assert ret is not val - - -def test_remove_defaults_removes_defaults(): - ret = remove_defaults({'key': False}, map_optional) - assert ret == {} - - -def test_remove_defaults_nothing_to_remove(): - ret = remove_defaults({}, map_optional) - assert ret == {} - - -def test_remove_defaults_does_not_change_non_default(): - ret = remove_defaults({'key': True}, map_optional) - assert ret == {'key': True} - - -def test_remove_defaults_map_in_list(): - ret = remove_defaults([{'key': False}], Array(map_optional)) - assert ret == [{}] - - -def test_remove_defaults_does_nothing_on_non_optional(): - ret = remove_defaults({'key': True}, map_required) - assert ret == {'key': True} - - -nested_schema_required = Map( - 'Repository', 'repo', - Required('repo', check_any), - RequiredRecurse('hooks', Array(map_required)), -) -nested_schema_optional = Map( - 'Repository', 'repo', - Required('repo', check_any), - RequiredRecurse('hooks', Array(map_optional)), -) - - -def test_validate_failure_nested(): - with pytest.raises(ValidationError) as excinfo: - validate({'repo': 1, 'hooks': [{}]}, nested_schema_required) - _assert_exception_trace( - excinfo.value, - ( - 'At Repository(repo=1)', 'At key: hooks', 'At foo(key=MISSING)', - 'Missing required key: key', - ), - ) - - -def test_apply_defaults_nested(): - val = {'repo': 'repo1', 'hooks': [{}]} - ret = apply_defaults(val, nested_schema_optional) - assert ret == {'repo': 'repo1', 'hooks': [{'key': False}]} - - -def test_remove_defaults_nested(): - val = {'repo': 'repo1', 'hooks': [{'key': False}]} - ret = remove_defaults(val, nested_schema_optional) - assert ret == {'repo': 'repo1', 'hooks': [{}]} - - -class Error(Exception): - pass - - -def test_load_from_filename_file_does_not_exist(): - with pytest.raises(Error) as excinfo: - load_from_filename('does_not_exist', map_required, json.loads, Error) - assert excinfo.value.args[0].error_msg == 'does_not_exist does not exist' - - -def test_load_from_filename_fails_load_strategy(tmpdir): - f = tmpdir.join('foo.notjson') - f.write('totes not json') - with pytest.raises(Error) as excinfo: - load_from_filename(f.strpath, map_required, json.loads, Error) - _assert_exception_trace( - excinfo.value.args[0], - # ANY is json's error message - ('File {}'.format(f.strpath), mock.ANY), - ) - - -def test_load_from_filename_validation_error(tmpdir): - f = tmpdir.join('foo.json') - f.write('{}') - with pytest.raises(Error) as excinfo: - load_from_filename(f.strpath, map_required, json.loads, Error) - _assert_exception_trace( - excinfo.value.args[0], - ( - 'File {}'.format(f.strpath), 'At foo(key=MISSING)', - 'Missing required key: key', - ), - ) - - -def test_load_from_filename_applies_defaults(tmpdir): - f = tmpdir.join('foo.json') - f.write('{}') - ret = load_from_filename(f.strpath, map_optional, json.loads, Error) - assert ret == {'key': False} From 98a6fce830f6dc0bb201b42b6ffc40da42376f9f Mon Sep 17 00:00:00 2001 From: Theresa Ma Date: Sat, 24 Feb 2018 11:19:13 -0800 Subject: [PATCH 152/544] update swift to swift 4 --- get-swift.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/get-swift.sh b/get-swift.sh index 667ef4c80..a45291e23 100755 --- a/get-swift.sh +++ b/get-swift.sh @@ -4,9 +4,9 @@ set -ex . /etc/lsb-release if [ "$DISTRIB_CODENAME" = "trusty" ]; then - SWIFT_URL='https://swift.org/builds/swift-3.1.1-release/ubuntu1404/swift-3.1.1-RELEASE/swift-3.1.1-RELEASE-ubuntu14.04.tar.gz' + SWIFT_URL='https://swift.org/builds/swift-4.0.3-release/ubuntu1404/swift-4.0.3-RELEASE/swift-4.0.3-RELEASE-ubuntu14.04.tar.gz' else - SWIFT_URL='https://swift.org/builds/swift-3.1.1-release/ubuntu1604/swift-3.1.1-RELEASE/swift-3.1.1-RELEASE-ubuntu16.04.tar.gz' + SWIFT_URL='https://swift.org/builds/swift-4.0.3-release/ubuntu1604/swift-4.0.3-RELEASE/swift-4.0.3-RELEASE-ubuntu16.04.tar.gz' fi mkdir -p /tmp/swift From 40fd04aec3e5f57cb0c4222f1c0e2f97a0c7bd6f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 24 Feb 2018 13:50:15 -0800 Subject: [PATCH 153/544] Don't modify user's npmrc under test --- tests/repository_test.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/repository_test.py b/tests/repository_test.py index 068d6bac7..0123ce4ca 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -517,8 +517,7 @@ def test_additional_node_dependencies_installed( repo = Repository.create(config, store) repo.require_installed() with node.in_env(repo._prefix, 'default'): - cmd_output('npm', 'config', 'set', 'global', 'true') - output = cmd_output('npm', 'ls')[1] + output = cmd_output('npm', 'ls', '-g')[1] assert 'lodash' in output From d7a41d88c39c448dd87c7906f21db799e3f63de3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 24 Feb 2018 14:36:18 -0800 Subject: [PATCH 154/544] Don't write to the home directory under test --- tests/commands/install_uninstall_test.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index ea6727e41..00d5eff4f 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -187,11 +187,14 @@ def test_unicode_merge_commit_message(tempdir_factory): with cwd(path): assert install(Runner(path, C.CONFIG_FILE)) == 0 cmd_output('git', 'checkout', 'master', '-b', 'foo') - cmd_output('git', 'commit', '--allow-empty', '-m', 'branch2') + cmd_output('git', 'commit', '--allow-empty', '-n', '-m', 'branch2') cmd_output('git', 'checkout', 'master') cmd_output('git', 'merge', 'foo', '--no-ff', '--no-commit', '-m', '☃') # Used to crash - cmd_output('git', 'commit', '--no-edit') + cmd_output_mocked_pre_commit_home( + 'git', 'commit', '--no-edit', + tempdir_factory=tempdir_factory, + ) def test_install_idempotent(tempdir_factory): From b827694520be0f39bfc0599f3680b6c08b4516cf Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 24 Feb 2018 14:29:32 -0800 Subject: [PATCH 155/544] Each set of additional dependencies gets its own env --- pre_commit/commands/autoupdate.py | 6 +-- pre_commit/repository.py | 64 +++++++------------------ pre_commit/store.py | 19 ++++---- tests/conftest.py | 6 --- tests/repository_test.py | 77 +++++++++++++------------------ 5 files changed, 61 insertions(+), 111 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index ca83a5880..666cd117b 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -33,9 +33,9 @@ def _update_repo(repo_config, runner, tags_only): Args: repo_config - A config for a repository """ - repo = Repository.create(repo_config, runner.store) + repo_path = runner.store.clone(repo_config['repo'], repo_config['sha']) - with cwd(repo._repo_path): + with cwd(repo_path): cmd_output('git', 'fetch') tag_cmd = ('git', 'describe', 'origin/master', '--tags') if tags_only: @@ -57,7 +57,7 @@ def _update_repo(repo_config, runner, tags_only): new_repo = Repository.create(new_config, runner.store) # See if any of our hooks were deleted with the new commits - hooks = {hook['id'] for hook in repo.repo_config['hooks']} + hooks = {hook['id'] for hook in repo_config['hooks']} hooks_missing = hooks - (hooks & set(new_repo.manifest_hooks)) if hooks_missing: raise RepositoryCannotBeUpdatedError( diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 3ed160af5..624ccd003 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -7,7 +7,6 @@ import pipes import shutil import sys -from collections import defaultdict import pkg_resources from cached_property import cached_property @@ -149,22 +148,11 @@ def create(cls, config, store): else: return cls(config, store) - @cached_property - def _repo_path(self): - return self.store.clone( - self.repo_config['repo'], self.repo_config['sha'], - ) - - @cached_property - def _prefix(self): - return Prefix(self._repo_path) - - def _prefix_from_deps(self, language_name, deps): - return self._prefix - @cached_property def manifest_hooks(self): - manifest_path = os.path.join(self._repo_path, C.MANIFEST_FILE) + repo, sha = self.repo_config['repo'], self.repo_config['sha'] + repo_path = self.store.clone(repo, sha) + manifest_path = os.path.join(repo_path, C.MANIFEST_FILE) return {hook['id']: hook for hook in load_manifest(manifest_path)} @cached_property @@ -185,21 +173,25 @@ def hooks(self): for hook in self.repo_config['hooks'] ) - @cached_property + def _prefix_from_deps(self, language_name, deps): + repo, sha = self.repo_config['repo'], self.repo_config['sha'] + return Prefix(self.store.clone(repo, sha, deps)) + def _venvs(self): - deps_dict = defaultdict(_UniqueList) - for _, hook in self.hooks: - deps_dict[(hook['language'], hook['language_version'])].update( - hook['additional_dependencies'], - ) ret = [] - for (language, version), deps in deps_dict.items(): - ret.append((self._prefix, language, version, deps)) + for _, hook in self.hooks: + language = hook['language'] + version = hook['language_version'] + deps = hook['additional_dependencies'] + ret.append(( + self._prefix_from_deps(language, deps), + language, version, deps, + )) return tuple(ret) def require_installed(self): if not self.__installed: - _install_all(self._venvs, self.repo_config['repo'], self.store) + _install_all(self._venvs(), self.repo_config['repo'], self.store) self.__installed = True def run_hook(self, hook, file_args): @@ -237,19 +229,6 @@ def hooks(self): for hook in self.repo_config['hooks'] ) - @cached_property - def _venvs(self): - ret = [] - for _, hook in self.hooks: - language = hook['language'] - version = hook['language_version'] - deps = hook['additional_dependencies'] - ret.append(( - self._prefix_from_deps(language, deps), - language, version, deps, - )) - return tuple(ret) - class MetaRepository(LocalRepository): @cached_property @@ -303,14 +282,3 @@ def hooks(self): (hook['id'], _hook(self.manifest_hooks[hook['id']], hook)) for hook in self.repo_config['hooks'] ) - - -class _UniqueList(list): - def __init__(self): - self._set = set() - - def update(self, obj): - for item in obj: - if item not in self._set: - self._set.add(item) - self.append(item) diff --git a/pre_commit/store.py b/pre_commit/store.py index 131198407..7e49c8fdb 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -72,9 +72,9 @@ def _write_sqlite_db(self): with contextlib.closing(sqlite3.connect(tmpfile)) as db: db.executescript( 'CREATE TABLE repos (' - ' repo CHAR(255) NOT NULL,' - ' ref CHAR(255) NOT NULL,' - ' path CHAR(255) NOT NULL,' + ' repo TEXT NOT NULL,' + ' ref TEXT NOT NULL,' + ' path TEXT NOT NULL,' ' PRIMARY KEY (repo, ref)' ');', ) @@ -101,15 +101,17 @@ def require_created(self): self._create() self.__created = True - def _new_repo(self, repo, ref, make_strategy): + def _new_repo(self, repo, ref, deps, make_strategy): self.require_created() + if deps: + repo = '{}:{}'.format(repo, ','.join(sorted(deps))) def _get_result(): # Check if we already exist with sqlite3.connect(self.db_path) as db: result = db.execute( 'SELECT path FROM repos WHERE repo = ? AND ref = ?', - [repo, ref], + (repo, ref), ).fetchone() if result: return result[0] @@ -137,7 +139,7 @@ def _get_result(): ) return directory - def clone(self, repo, ref): + def clone(self, repo, ref, deps=()): """Clone the given url and checkout the specific ref.""" def clone_strategy(directory): cmd_output( @@ -151,7 +153,7 @@ def clone_strategy(directory): env=no_git_env(), ) - return self._new_repo(repo, ref, clone_strategy) + return self._new_repo(repo, ref, deps, clone_strategy) def make_local(self, deps): def make_local_strategy(directory): @@ -172,8 +174,7 @@ def _git_cmd(*args): _git_cmd('commit', '--no-edit', '--no-gpg-sign', '-n', '-minit') return self._new_repo( - 'local:{}'.format(','.join(sorted(deps))), C.LOCAL_REPO_VERSION, - make_local_strategy, + 'local', C.LOCAL_REPO_VERSION, deps, make_local_strategy, ) @cached_property diff --git a/tests/conftest.py b/tests/conftest.py index fd3784dfa..246820e94 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -165,12 +165,6 @@ def log_info_mock(): yield mck -@pytest.fixture -def log_warning_mock(): - with mock.patch.object(logging.getLogger('pre_commit'), 'warning') as mck: - yield mck - - class FakeStream(object): def __init__(self): self.data = io.BytesIO() diff --git a/tests/repository_test.py b/tests/repository_test.py index 0123ce4ca..dea387f21 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -433,7 +433,7 @@ def test_venvs(tempdir_factory, store): path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) repo = Repository.create(config, store) - venv, = repo._venvs + venv, = repo._venvs() assert venv == (mock.ANY, 'python', python.get_default_version(), []) @@ -443,50 +443,33 @@ def test_additional_dependencies(tempdir_factory, store): config = make_config_from_repo(path) config['hooks'][0]['additional_dependencies'] = ['pep8'] repo = Repository.create(config, store) - venv, = repo._venvs + venv, = repo._venvs() assert venv == (mock.ANY, 'python', python.get_default_version(), ['pep8']) @pytest.mark.integration -def test_additional_dependencies_duplicated( - tempdir_factory, store, log_warning_mock, -): - path = make_repo(tempdir_factory, 'ruby_hooks_repo') - config = make_config_from_repo(path) - deps = ['thread_safe', 'tins', 'thread_safe'] - config['hooks'][0]['additional_dependencies'] = deps - repo = Repository.create(config, store) - venv, = repo._venvs - assert venv == (mock.ANY, 'ruby', 'default', ['thread_safe', 'tins']) - - -@pytest.mark.integration -def test_additional_python_dependencies_installed(tempdir_factory, store): +def test_additional_dependencies_roll_forward(tempdir_factory, store): path = make_repo(tempdir_factory, 'python_hooks_repo') - config = make_config_from_repo(path) - config['hooks'][0]['additional_dependencies'] = ['mccabe'] - repo = Repository.create(config, store) - repo.require_installed() - with python.in_env(repo._prefix, 'default'): - output = cmd_output('pip', 'freeze', '-l')[1] - assert 'mccabe' in output + config1 = make_config_from_repo(path) + repo1 = Repository.create(config1, store) + repo1.require_installed() + (prefix1, _, version1, _), = repo1._venvs() + with python.in_env(prefix1, version1): + assert 'mccabe' not in cmd_output('pip', 'freeze', '-l')[1] -@pytest.mark.integration -def test_additional_dependencies_roll_forward(tempdir_factory, store): - path = make_repo(tempdir_factory, 'python_hooks_repo') - config = make_config_from_repo(path) - # Run the repo once without additional_dependencies - repo = Repository.create(config, store) - repo.require_installed() - # Now run it with additional_dependencies - config['hooks'][0]['additional_dependencies'] = ['mccabe'] - repo = Repository.create(config, store) - repo.require_installed() - # We should see our additional dependency installed - with python.in_env(repo._prefix, 'default'): - output = cmd_output('pip', 'freeze', '-l')[1] - assert 'mccabe' in output + # Make another repo with additional dependencies + config2 = make_config_from_repo(path) + config2['hooks'][0]['additional_dependencies'] = ['mccabe'] + repo2 = Repository.create(config2, store) + repo2.require_installed() + (prefix2, _, version2, _), = repo2._venvs() + with python.in_env(prefix2, version2): + assert 'mccabe' in cmd_output('pip', 'freeze', '-l')[1] + + # should not have affected original + with python.in_env(prefix1, version1): + assert 'mccabe' not in cmd_output('pip', 'freeze', '-l')[1] @xfailif_windows_no_ruby @@ -499,7 +482,8 @@ def test_additional_ruby_dependencies_installed( config['hooks'][0]['additional_dependencies'] = ['thread_safe', 'tins'] repo = Repository.create(config, store) repo.require_installed() - with ruby.in_env(repo._prefix, 'default'): + (prefix, _, version, _), = repo._venvs() + with ruby.in_env(prefix, version): output = cmd_output('gem', 'list', '--local')[1] assert 'thread_safe' in output assert 'tins' in output @@ -516,7 +500,8 @@ def test_additional_node_dependencies_installed( config['hooks'][0]['additional_dependencies'] = ['lodash'] repo = Repository.create(config, store) repo.require_installed() - with node.in_env(repo._prefix, 'default'): + (prefix, _, version, _), = repo._venvs() + with node.in_env(prefix, version): output = cmd_output('npm', 'ls', '-g')[1] assert 'lodash' in output @@ -532,7 +517,8 @@ def test_additional_golang_dependencies_installed( config['hooks'][0]['additional_dependencies'] = deps repo = Repository.create(config, store) repo.require_installed() - binaries = os.listdir(repo._prefix.path( + (prefix, _, _, _), = repo._venvs() + binaries = os.listdir(prefix.path( helpers.environment_dir(golang.ENVIRONMENT_DIR, 'default'), 'bin', )) # normalize for windows @@ -598,8 +584,9 @@ class MyKeyboardInterrupt(KeyboardInterrupt): repo.run_hook(hook, []) # Should have made an environment, however this environment is broken! - envdir = 'py_env-{}'.format(python.get_default_version()) - assert repo._prefix.exists(envdir) + (prefix, _, version, _), = repo._venvs() + envdir = 'py_env-{}'.format(version) + assert prefix.exists(envdir) # However, it should be perfectly runnable (reinstall after botched # install) @@ -616,8 +603,8 @@ def test_invalidated_virtualenv(tempdir_factory, store): # Simulate breaking of the virtualenv repo.require_installed() - version = python.get_default_version() - libdir = repo._prefix.path('py_env-{}'.format(version), 'lib', version) + (prefix, _, version, _), = repo._venvs() + libdir = prefix.path('py_env-{}'.format(version), 'lib', version) paths = [ os.path.join(libdir, p) for p in ('site.py', 'site.pyc', '__pycache__') ] From f76d3c4f9505007e654f237f84e3014473409b22 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 24 Feb 2018 15:42:26 -0800 Subject: [PATCH 156/544] Allow autoupdate --repo to be specified multiple times --- pre_commit/commands/autoupdate.py | 4 ++-- pre_commit/main.py | 5 +++-- tests/commands/autoupdate_test.py | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index ca83a5880..f375913c7 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -106,7 +106,7 @@ def _write_new_config_file(path, output): f.write(to_write) -def autoupdate(runner, tags_only, repo=None): +def autoupdate(runner, tags_only, repos=()): """Auto-update the pre-commit config to the latest versions of repos.""" migrate_config(runner, quiet=True) retv = 0 @@ -120,7 +120,7 @@ def autoupdate(runner, tags_only, repo=None): is_local_repo(repo_config) or is_meta_repo(repo_config) or # Skip updating any repo_configs that aren't for the specified repo - repo and repo != repo_config['repo'] + repos and repo_config['repo'] not in repos ): output_repos.append(repo_config) continue diff --git a/pre_commit/main.py b/pre_commit/main.py index 16b6c3b61..e2f48ed34 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -168,7 +168,8 @@ def main(argv=None): ), ) autoupdate_parser.add_argument( - '--repo', help='Only update this repository.', + '--repo', dest='repos', action='append', metavar='REPO', + help='Only update this repository -- may be specified multiple times.', ) migrate_config_parser = subparsers.add_parser( @@ -251,7 +252,7 @@ def main(argv=None): return autoupdate( runner, tags_only=not args.bleeding_edge, - repo=args.repo, + repos=args.repos, ) elif args.command == 'migrate-config': return migrate_config(runner) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 91e7733ff..8fe4583de 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -138,7 +138,7 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( runner = Runner('.', C.CONFIG_FILE) before = open(C.CONFIG_FILE).read() repo_name = 'file://{}'.format(out_of_date_repo.path) - ret = autoupdate(runner, tags_only=False, repo=repo_name) + ret = autoupdate(runner, tags_only=False, repos=(repo_name,)) after = open(C.CONFIG_FILE).read() assert ret == 0 assert before != after @@ -158,7 +158,7 @@ def test_autoupdate_out_of_date_repo_with_wrong_repo_name( runner = Runner('.', C.CONFIG_FILE) before = open(C.CONFIG_FILE).read() # It will not update it, because the name doesn't match - ret = autoupdate(runner, tags_only=False, repo='wrong_repo_name') + ret = autoupdate(runner, tags_only=False, repos=('wrong_repo_name',)) after = open(C.CONFIG_FILE).read() assert ret == 0 assert before == after From 29033f10caadb816f77a6a8826ae9d780b2684ce Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 24 Feb 2018 16:44:59 -0800 Subject: [PATCH 157/544] Move cwd() to tests-only --- pre_commit/commands/autoupdate.py | 23 ++++++------- pre_commit/make_archives.py | 4 +-- pre_commit/store.py | 19 +++++----- pre_commit/util.py | 10 ------ testing/fixtures.py | 16 ++++----- testing/util.py | 11 ++++++ tests/commands/autoupdate_test.py | 40 ++++++++++------------ tests/commands/install_uninstall_test.py | 7 ++-- tests/commands/run_test.py | 2 +- tests/commands/try_repo_test.py | 2 +- tests/conftest.py | 15 ++++---- tests/git_test.py | 2 +- tests/main_test.py | 2 +- tests/make_archives_test.py | 20 +++++------ tests/meta_hooks/check_hooks_apply_test.py | 2 +- tests/meta_hooks/useless_excludes_test.py | 2 +- tests/repository_test.py | 2 +- tests/runner_test.py | 2 +- tests/staged_files_only_test.py | 2 +- tests/store_test.py | 2 +- tests/util_test.py | 2 +- 21 files changed, 84 insertions(+), 103 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 666cd117b..ce6fc34a6 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -18,7 +18,6 @@ from pre_commit.repository import Repository from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output -from pre_commit.util import cwd class RepositoryCannotBeUpdatedError(RuntimeError): @@ -35,17 +34,17 @@ def _update_repo(repo_config, runner, tags_only): """ repo_path = runner.store.clone(repo_config['repo'], repo_config['sha']) - with cwd(repo_path): - cmd_output('git', 'fetch') - tag_cmd = ('git', 'describe', 'origin/master', '--tags') - if tags_only: - tag_cmd += ('--abbrev=0',) - else: - tag_cmd += ('--exact',) - try: - rev = cmd_output(*tag_cmd)[1].strip() - except CalledProcessError: - rev = cmd_output('git', 'rev-parse', 'origin/master')[1].strip() + cmd_output('git', '-C', repo_path, 'fetch') + tag_cmd = ('git', '-C', repo_path, 'describe', 'origin/master', '--tags') + if tags_only: + tag_cmd += ('--abbrev=0',) + else: + tag_cmd += ('--exact',) + try: + rev = cmd_output(*tag_cmd)[1].strip() + except CalledProcessError: + tag_cmd = ('git', '-C', repo_path, 'rev-parse', 'origin/master') + rev = cmd_output(*tag_cmd)[1].strip() # Don't bother trying to update if our sha is the same if rev == repo_config['sha']: diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index 90809c106..2e7658da5 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -8,7 +8,6 @@ from pre_commit import output from pre_commit.util import cmd_output -from pre_commit.util import cwd from pre_commit.util import resource_filename from pre_commit.util import rmtree from pre_commit.util import tmpdir @@ -42,8 +41,7 @@ def make_archive(name, repo, ref, destdir): with tmpdir() as tempdir: # Clone the repository to the temporary directory cmd_output('git', 'clone', repo, tempdir) - with cwd(tempdir): - cmd_output('git', 'checkout', ref) + cmd_output('git', '-C', tempdir, 'checkout', ref) # We don't want the '.git' directory # It adds a bunch of size to the archive and we don't use it at diff --git a/pre_commit/store.py b/pre_commit/store.py index 7e49c8fdb..735d67cf6 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -14,7 +14,6 @@ from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import copy_tree_to_path -from pre_commit.util import cwd from pre_commit.util import no_git_env from pre_commit.util import resource_filename @@ -142,16 +141,14 @@ def _get_result(): def clone(self, repo, ref, deps=()): """Clone the given url and checkout the specific ref.""" def clone_strategy(directory): - cmd_output( - 'git', 'clone', '--no-checkout', repo, directory, - env=no_git_env(), - ) - with cwd(directory): - cmd_output('git', 'reset', ref, '--hard', env=no_git_env()) - cmd_output( - 'git', 'submodule', 'update', '--init', '--recursive', - env=no_git_env(), - ) + env = no_git_env() + + def _git_cmd(*args): + return cmd_output('git', '-C', directory, *args, env=env) + + _git_cmd('clone', '--no-checkout', repo, '.') + _git_cmd('reset', ref, '--hard') + _git_cmd('submodule', 'update', '--init', '--recursive') return self._new_repo(repo, ref, deps, clone_strategy) diff --git a/pre_commit/util.py b/pre_commit/util.py index 081adf27c..882ebb006 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -16,16 +16,6 @@ from pre_commit import parse_shebang -@contextlib.contextmanager -def cwd(path): - original_cwd = os.getcwd() - os.chdir(path) - try: - yield - finally: - os.chdir(original_cwd) - - def mkdirp(path): try: os.makedirs(path) diff --git a/testing/fixtures.py b/testing/fixtures.py index bff32805d..3537ca713 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -17,7 +17,6 @@ from pre_commit.clientlib import load_manifest from pre_commit.util import cmd_output from pre_commit.util import copy_tree_to_path -from pre_commit.util import cwd from testing.util import get_resource_path @@ -30,9 +29,8 @@ def git_dir(tempdir_factory): def make_repo(tempdir_factory, repo_source): path = git_dir(tempdir_factory) copy_tree_to_path(get_resource_path(repo_source), path) - with cwd(path): - cmd_output('git', 'add', '.') - cmd_output('git', 'commit', '-m', 'Add hooks') + cmd_output('git', '-C', path, 'add', '.') + cmd_output('git', '-C', path, 'commit', '-m', 'Add hooks') return path @@ -116,17 +114,15 @@ def write_config(directory, config, config_file=C.CONFIG_FILE): def add_config_to_repo(git_path, config, config_file=C.CONFIG_FILE): write_config(git_path, config, config_file=config_file) - with cwd(git_path): - cmd_output('git', 'add', config_file) - cmd_output('git', 'commit', '-m', 'Add hooks config') + cmd_output('git', '-C', git_path, 'add', config_file) + cmd_output('git', '-C', git_path, 'commit', '-m', 'Add hooks config') return git_path def remove_config_from_repo(git_path, config_file=C.CONFIG_FILE): os.unlink(os.path.join(git_path, config_file)) - with cwd(git_path): - cmd_output('git', 'add', config_file) - cmd_output('git', 'commit', '-m', 'Remove hooks config') + cmd_output('git', '-C', git_path, 'add', config_file) + cmd_output('git', '-C', git_path, 'commit', '-m', 'Remove hooks config') return git_path diff --git a/testing/util.py b/testing/util.py index aa4b76f50..025bc0bba 100644 --- a/testing/util.py +++ b/testing/util.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import contextlib import os.path import sys @@ -103,3 +104,13 @@ def run_opts( show_diff_on_failure=show_diff_on_failure, commit_msg_filename=commit_msg_filename, ) + + +@contextlib.contextmanager +def cwd(path): + original_cwd = os.getcwd() + os.chdir(path) + try: + yield + finally: + os.chdir(original_cwd) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 91e7733ff..11c717056 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import os.path import pipes import shutil from collections import OrderedDict @@ -14,7 +15,6 @@ from pre_commit.commands.autoupdate import RepositoryCannotBeUpdatedError from pre_commit.runner import Runner from pre_commit.util import cmd_output -from pre_commit.util import cwd from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import add_config_to_repo from testing.fixtures import config_with_local_hooks @@ -62,14 +62,13 @@ def test_autoupdate_old_revision_broken( path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path, check=False) - with cwd(path): - cmd_output('git', 'mv', C.MANIFEST_FILE, 'nope.yaml') - cmd_output('git', 'commit', '-m', 'simulate old repo') - # Assume this is the revision the user's old repository was at - rev = git.head_sha(path) - cmd_output('git', 'mv', 'nope.yaml', C.MANIFEST_FILE) - cmd_output('git', 'commit', '-m', 'move hooks file') - update_rev = git.head_sha(path) + cmd_output('git', '-C', path, 'mv', C.MANIFEST_FILE, 'nope.yaml') + cmd_output('git', '-C', path, 'commit', '-m', 'simulate old repo') + # Assume this is the revision the user's old repository was at + rev = git.head_sha(path) + cmd_output('git', '-C', path, 'mv', 'nope.yaml', C.MANIFEST_FILE) + cmd_output('git', '-C', path, 'commit', '-m', 'move hooks file') + update_rev = git.head_sha(path) config['sha'] = rev write_config('.', config) @@ -87,8 +86,7 @@ def out_of_date_repo(tempdir_factory): original_sha = git.head_sha(path) # Make a commit - with cwd(path): - cmd_output('git', 'commit', '--allow-empty', '-m', 'foo') + cmd_output('git', '-C', path, 'commit', '--allow-empty', '-m', 'foo') head_sha = git.head_sha(path) yield auto_namedtuple( @@ -223,8 +221,7 @@ def test_loses_formatting_when_not_detectable( @pytest.fixture def tagged_repo(out_of_date_repo): - with cwd(out_of_date_repo.path): - cmd_output('git', 'tag', 'v1.2.3') + cmd_output('git', '-C', out_of_date_repo.path, 'tag', 'v1.2.3') yield out_of_date_repo @@ -243,8 +240,8 @@ def test_autoupdate_tagged_repo( @pytest.fixture def tagged_repo_with_more_commits(tagged_repo): - with cwd(tagged_repo.path): - cmd_output('git', 'commit', '--allow-empty', '-m', 'commit!') + cmd = ('git', '-C', tagged_repo.path, 'commit', '--allow-empty', '-mfoo') + cmd_output(*cmd) yield tagged_repo @@ -267,13 +264,12 @@ def hook_disappearing_repo(tempdir_factory): path = make_repo(tempdir_factory, 'python_hooks_repo') original_sha = git.head_sha(path) - with cwd(path): - shutil.copy( - get_resource_path('manifest_without_foo.yaml'), - C.MANIFEST_FILE, - ) - cmd_output('git', 'add', '.') - cmd_output('git', 'commit', '-m', 'Remove foo') + shutil.copy( + get_resource_path('manifest_without_foo.yaml'), + os.path.join(path, C.MANIFEST_FILE), + ) + cmd_output('git', '-C', path, 'add', '.') + cmd_output('git', '-C', path, 'commit', '-m', 'Remove foo') yield auto_namedtuple(path=path, original_sha=original_sha) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 00d5eff4f..a49a3e4f3 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -20,7 +20,6 @@ from pre_commit.commands.install_uninstall import uninstall from pre_commit.runner import Runner from pre_commit.util import cmd_output -from pre_commit.util import cwd from pre_commit.util import make_executable from pre_commit.util import mkdirp from pre_commit.util import resource_filename @@ -28,6 +27,7 @@ from testing.fixtures import make_consuming_repo from testing.fixtures import remove_config_from_repo from testing.util import cmd_output_mocked_pre_commit_home +from testing.util import cwd from testing.util import xfailif_no_symlink @@ -153,9 +153,8 @@ def test_install_pre_commit_and_run_custom_path(tempdir_factory): def test_install_in_submodule_and_run(tempdir_factory): src_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') parent_path = git_dir(tempdir_factory) - with cwd(parent_path): - cmd_output('git', 'submodule', 'add', src_path, 'sub') - cmd_output('git', 'commit', '-m', 'foo') + cmd_output('git', '-C', parent_path, 'submodule', 'add', src_path, 'sub') + cmd_output('git', '-C', parent_path, 'commit', '-m', 'foo') sub_pth = os.path.join(parent_path, 'sub') with cwd(sub_pth): diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 94dd52199..8107e79ab 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -18,13 +18,13 @@ from pre_commit.commands.run import run from pre_commit.runner import Runner from pre_commit.util import cmd_output -from pre_commit.util import cwd from pre_commit.util import make_executable from testing.fixtures import add_config_to_repo from testing.fixtures import make_consuming_repo from testing.fixtures import modify_config from testing.fixtures import read_config from testing.util import cmd_output_mocked_pre_commit_home +from testing.util import cwd from testing.util import run_opts from testing.util import xfailif_no_symlink diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index e530dee80..a29181b83 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -5,10 +5,10 @@ from pre_commit.commands.try_repo import try_repo from pre_commit.util import cmd_output -from pre_commit.util import cwd from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import git_dir from testing.fixtures import make_repo +from testing.util import cwd from testing.util import run_opts diff --git a/tests/conftest.py b/tests/conftest.py index 246820e94..678010f50 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,10 +17,10 @@ from pre_commit.runner import Runner from pre_commit.store import Store from pre_commit.util import cmd_output -from pre_commit.util import cwd from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo from testing.fixtures import write_config +from testing.util import cwd @pytest.fixture @@ -68,10 +68,9 @@ def _make_conflict(): @pytest.fixture def in_merge_conflict(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(path): - open('dummy', 'a').close() - cmd_output('git', 'add', 'dummy') - cmd_output('git', 'commit', '-m', 'Add config.') + open(os.path.join(path, 'dummy'), 'a').close() + cmd_output('git', '-C', path, 'add', 'dummy') + cmd_output('git', '-C', path, 'commit', '-m', 'Add config.') conflict_path = tempdir_factory.get() cmd_output('git', 'clone', path, conflict_path) @@ -84,10 +83,8 @@ def in_merge_conflict(tempdir_factory): def in_conflicting_submodule(tempdir_factory): git_dir_1 = git_dir(tempdir_factory) git_dir_2 = git_dir(tempdir_factory) - with cwd(git_dir_2): - cmd_output('git', 'commit', '--allow-empty', '-m', 'init!') - with cwd(git_dir_1): - cmd_output('git', 'submodule', 'add', git_dir_2, 'sub') + cmd_output('git', '-C', git_dir_2, 'commit', '--allow-empty', '-minit!') + cmd_output('git', '-C', git_dir_1, 'submodule', 'add', git_dir_2, 'sub') with cwd(os.path.join(git_dir_1, 'sub')): _make_conflict() yield diff --git a/tests/git_test.py b/tests/git_test.py index 8f80dcade..58f14f50a 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -9,8 +9,8 @@ from pre_commit import git from pre_commit.error_handler import FatalError from pre_commit.util import cmd_output -from pre_commit.util import cwd from testing.fixtures import git_dir +from testing.util import cwd def test_get_root_at_root(tempdir_factory): diff --git a/tests/main_test.py b/tests/main_test.py index deb3ba18a..ae6a73e7f 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -8,8 +8,8 @@ import pytest from pre_commit import main -from pre_commit.util import cwd from testing.auto_namedtuple import auto_namedtuple +from testing.util import cwd FNS = ( diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py index 2cb626971..414f853c1 100644 --- a/tests/make_archives_test.py +++ b/tests/make_archives_test.py @@ -9,7 +9,6 @@ from pre_commit import git from pre_commit import make_archives from pre_commit.util import cmd_output -from pre_commit.util import cwd from testing.fixtures import git_dir @@ -17,16 +16,15 @@ def test_make_archive(tempdir_factory): output_dir = tempdir_factory.get() git_path = git_dir(tempdir_factory) # Add a files to the git directory - with cwd(git_path): - open('foo', 'a').close() - cmd_output('git', 'add', '.') - cmd_output('git', 'commit', '-m', 'foo') - # We'll use this sha - head_sha = git.head_sha('.') - # And check that this file doesn't exist - open('bar', 'a').close() - cmd_output('git', 'add', '.') - cmd_output('git', 'commit', '-m', 'bar') + open(os.path.join(git_path, 'foo'), 'a').close() + cmd_output('git', '-C', git_path, 'add', '.') + cmd_output('git', '-C', git_path, 'commit', '-m', 'foo') + # We'll use this sha + head_sha = git.head_sha(git_path) + # And check that this file doesn't exist + open(os.path.join(git_path, 'bar'), 'a').close() + cmd_output('git', '-C', git_path, 'add', '.') + cmd_output('git', '-C', git_path, 'commit', '-m', 'bar') # Do the thing archive_path = make_archives.make_archive( diff --git a/tests/meta_hooks/check_hooks_apply_test.py b/tests/meta_hooks/check_hooks_apply_test.py index 86bc598dd..c777daa87 100644 --- a/tests/meta_hooks/check_hooks_apply_test.py +++ b/tests/meta_hooks/check_hooks_apply_test.py @@ -1,9 +1,9 @@ from collections import OrderedDict from pre_commit.meta_hooks import check_hooks_apply -from pre_commit.util import cwd from testing.fixtures import add_config_to_repo from testing.fixtures import git_dir +from testing.util import cwd def test_hook_excludes_everything( diff --git a/tests/meta_hooks/useless_excludes_test.py b/tests/meta_hooks/useless_excludes_test.py index 08b87aa8a..137c357f3 100644 --- a/tests/meta_hooks/useless_excludes_test.py +++ b/tests/meta_hooks/useless_excludes_test.py @@ -1,9 +1,9 @@ from collections import OrderedDict from pre_commit.meta_hooks import check_useless_excludes -from pre_commit.util import cwd from testing.fixtures import add_config_to_repo from testing.fixtures import git_dir +from testing.util import cwd def test_useless_exclude_global(capsys, tempdir_factory): diff --git a/tests/repository_test.py b/tests/repository_test.py index dea387f21..7f0593bfd 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -22,12 +22,12 @@ from pre_commit.languages import ruby from pre_commit.repository import Repository from pre_commit.util import cmd_output -from pre_commit.util import cwd from testing.fixtures import config_with_local_hooks from testing.fixtures import git_dir from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo from testing.fixtures import modify_manifest +from testing.util import cwd from testing.util import get_resource_path from testing.util import skipif_cant_run_docker from testing.util import skipif_cant_run_swift diff --git a/tests/runner_test.py b/tests/runner_test.py index b5c0ce756..df3247125 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -7,10 +7,10 @@ import pre_commit.constants as C from pre_commit.runner import Runner from pre_commit.util import cmd_output -from pre_commit.util import cwd from testing.fixtures import add_config_to_repo from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo +from testing.util import cwd def test_init_has_no_side_effects(tmpdir): diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index d4dfadd60..481a2886a 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -11,9 +11,9 @@ from pre_commit.staged_files_only import staged_files_only from pre_commit.util import cmd_output -from pre_commit.util import cwd from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import git_dir +from testing.util import cwd from testing.util import get_resource_path diff --git a/tests/store_test.py b/tests/store_test.py index deb22bb8e..86c3ec44c 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -13,9 +13,9 @@ from pre_commit.store import _get_default_directory from pre_commit.store import Store from pre_commit.util import cmd_output -from pre_commit.util import cwd from pre_commit.util import rmtree from testing.fixtures import git_dir +from testing.util import cwd def test_our_session_fixture_works(): diff --git a/tests/util_test.py b/tests/util_test.py index 156148d58..967163e46 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -8,9 +8,9 @@ from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output -from pre_commit.util import cwd from pre_commit.util import memoize_by_cwd from pre_commit.util import tmpdir +from testing.util import cwd def test_CalledProcessError_str(): From 5651c66995e2912fbd588ad35aac790ffb1b1a8c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 24 Feb 2018 18:42:51 -0800 Subject: [PATCH 158/544] Migrate sha -> rev --- .pre-commit-config.yaml | 6 +-- pre_commit/clientlib.py | 37 +++++++++++++--- pre_commit/commands/autoupdate.py | 38 ++++++++-------- pre_commit/commands/migrate_config.py | 22 ++++++++-- pre_commit/commands/sample_config.py | 2 +- pre_commit/commands/try_repo.py | 4 +- pre_commit/git.py | 2 +- pre_commit/main.py | 4 +- pre_commit/repository.py | 8 ++-- testing/fixtures.py | 4 +- tests/clientlib_test.py | 51 ++++++++++++++++++++-- tests/commands/autoupdate_test.py | 62 +++++++++++++-------------- tests/commands/migrate_config_test.py | 27 ++++++++++++ tests/commands/sample_config_test.py | 2 +- tests/commands/try_repo_test.py | 4 +- tests/make_archives_test.py | 6 +-- tests/repository_test.py | 4 +- tests/staged_files_only_test.py | 27 ++++++------ tests/store_test.py | 12 +++--- 19 files changed, 215 insertions(+), 107 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 11f0ac29c..e4888a0b9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,16 +13,16 @@ repos: - id: requirements-txt-fixer - id: flake8 - repo: https://github.com/pre-commit/pre-commit.git - sha: v0.16.3 + rev: v0.16.3 hooks: - id: validate_manifest - repo: https://github.com/asottile/reorder_python_imports.git - sha: v0.3.5 + rev: v0.3.5 hooks: - id: reorder-python-imports language_version: python2.7 - repo: https://github.com/asottile/add-trailing-comma - sha: v0.6.4 + rev: v0.6.4 hooks: - id: add-trailing-comma - repo: meta diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index bb772341a..f6f86191e 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -93,6 +93,36 @@ def validate_manifest_main(argv=None): _LOCAL_SENTINEL = 'local' _META_SENTINEL = 'meta' + +class MigrateShaToRev(object): + @staticmethod + def _cond(key): + return cfgv.Conditional( + key, cfgv.check_string, + condition_key='repo', + condition_value=cfgv.NotIn(_LOCAL_SENTINEL, _META_SENTINEL), + ensure_absent=True, + ) + + def check(self, dct): + if dct.get('repo') in {_LOCAL_SENTINEL, _META_SENTINEL}: + self._cond('rev').check(dct) + self._cond('sha').check(dct) + elif 'sha' in dct and 'rev' in dct: + raise cfgv.ValidationError('Cannot specify both sha and rev') + elif 'sha' in dct: + self._cond('sha').check(dct) + else: + self._cond('rev').check(dct) + + def apply_default(self, dct): + if 'sha' in dct: + dct['rev'] = dct.pop('sha') + + def remove_default(self, dct): + pass + + CONFIG_HOOK_DICT = cfgv.Map( 'Hook', 'id', @@ -114,12 +144,7 @@ def validate_manifest_main(argv=None): cfgv.Required('repo', cfgv.check_string), cfgv.RequiredRecurse('hooks', cfgv.Array(CONFIG_HOOK_DICT)), - cfgv.Conditional( - 'sha', cfgv.check_string, - condition_key='repo', - condition_value=cfgv.NotIn(_LOCAL_SENTINEL, _META_SENTINEL), - ensure_absent=True, - ), + MigrateShaToRev(), ) CONFIG_SCHEMA = cfgv.Map( 'Config', None, diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 9da402780..cdaccfca2 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -32,7 +32,7 @@ def _update_repo(repo_config, runner, tags_only): Args: repo_config - A config for a repository """ - repo_path = runner.store.clone(repo_config['repo'], repo_config['sha']) + repo_path = runner.store.clone(repo_config['repo'], repo_config['rev']) cmd_output('git', '-C', repo_path, 'fetch') tag_cmd = ('git', '-C', repo_path, 'describe', 'origin/master', '--tags') @@ -46,13 +46,13 @@ def _update_repo(repo_config, runner, tags_only): tag_cmd = ('git', '-C', repo_path, 'rev-parse', 'origin/master') rev = cmd_output(*tag_cmd)[1].strip() - # Don't bother trying to update if our sha is the same - if rev == repo_config['sha']: + # Don't bother trying to update if our rev is the same + if rev == repo_config['rev']: return repo_config - # Construct a new config with the head sha + # Construct a new config with the head rev new_config = OrderedDict(repo_config) - new_config['sha'] = rev + new_config['rev'] = rev new_repo = Repository.create(new_config, runner.store) # See if any of our hooks were deleted with the new commits @@ -67,8 +67,8 @@ def _update_repo(repo_config, runner, tags_only): return new_config -SHA_LINE_RE = re.compile(r'^(\s+)sha:(\s*)([^\s#]+)(.*)$', re.DOTALL) -SHA_LINE_FMT = '{}sha:{}{}{}' +REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([^\s#]+)(.*)$', re.DOTALL) +REV_LINE_FMT = '{}rev:{}{}{}' def _write_new_config_file(path, output): @@ -77,25 +77,25 @@ def _write_new_config_file(path, output): new_contents = ordered_dump(output, **C.YAML_DUMP_KWARGS) lines = original_contents.splitlines(True) - sha_line_indices_rev = list(reversed([ - i for i, line in enumerate(lines) if SHA_LINE_RE.match(line) + rev_line_indices_reversed = list(reversed([ + i for i, line in enumerate(lines) if REV_LINE_RE.match(line) ])) for line in new_contents.splitlines(True): - if SHA_LINE_RE.match(line): - # It's possible we didn't identify the sha lines in the original - if not sha_line_indices_rev: + if REV_LINE_RE.match(line): + # It's possible we didn't identify the rev lines in the original + if not rev_line_indices_reversed: break - line_index = sha_line_indices_rev.pop() + line_index = rev_line_indices_reversed.pop() original_line = lines[line_index] - orig_match = SHA_LINE_RE.match(original_line) - new_match = SHA_LINE_RE.match(line) - lines[line_index] = SHA_LINE_FMT.format( + orig_match = REV_LINE_RE.match(original_line) + new_match = REV_LINE_RE.match(line) + lines[line_index] = REV_LINE_FMT.format( orig_match.group(1), orig_match.group(2), new_match.group(3), orig_match.group(4), ) - # If we failed to intelligently rewrite the sha lines, fall back to the + # If we failed to intelligently rewrite the rev lines, fall back to the # pretty-formatted yaml output to_write = ''.join(lines) if remove_defaults(ordered_load(to_write), CONFIG_SCHEMA) != output: @@ -132,10 +132,10 @@ def autoupdate(runner, tags_only, repos=()): retv = 1 continue - if new_repo_config['sha'] != repo_config['sha']: + if new_repo_config['rev'] != repo_config['rev']: changed = True output.write_line('updating {} -> {}.'.format( - repo_config['sha'], new_repo_config['sha'], + repo_config['rev'], new_repo_config['rev'], )) output_repos.append(new_repo_config) else: diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index 50f0c2da0..193a002bc 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import io +import re import yaml from aspy.yaml import ordered_load @@ -16,10 +17,7 @@ def _is_header_line(line): return (line.startswith(('#', '---')) or not line.strip()) -def migrate_config(runner, quiet=False): - with io.open(runner.config_file_path) as f: - contents = f.read() - +def _migrate_map(contents): # Find the first non-header line lines = contents.splitlines(True) i = 0 @@ -39,6 +37,22 @@ def migrate_config(runner, quiet=False): except yaml.YAMLError: contents = header + 'repos:\n' + _indent(rest) + return contents + + +def _migrate_sha_to_rev(contents): + reg = re.compile(r'(\n\s+)sha:') + return reg.sub(r'\1rev:', contents) + + +def migrate_config(runner, quiet=False): + with io.open(runner.config_file_path) as f: + orig_contents = contents = f.read() + + contents = _migrate_map(contents) + contents = _migrate_sha_to_rev(contents) + + if contents != orig_contents: with io.open(runner.config_file_path, 'w') as f: f.write(contents) diff --git a/pre_commit/commands/sample_config.py b/pre_commit/commands/sample_config.py index ae5946856..aef0107e8 100644 --- a/pre_commit/commands/sample_config.py +++ b/pre_commit/commands/sample_config.py @@ -12,7 +12,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - sha: v0.9.2 + rev: v1.2.1-1 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index 4c8258231..681543169 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -17,7 +17,7 @@ def try_repo(args): - ref = args.ref or git.head_sha(args.repo) + ref = args.ref or git.head_rev(args.repo) with tmpdir() as tempdir: if args.hook: @@ -28,7 +28,7 @@ def try_repo(args): manifest = sorted(manifest, key=lambda hook: hook['id']) hooks = [{'id': hook['id']} for hook in manifest] - items = (('repo', args.repo), ('sha', ref), ('hooks', hooks)) + items = (('repo', args.repo), ('rev', ref), ('hooks', hooks)) config = {'repos': [collections.OrderedDict(items)]} config_s = ordered_dump(config, **C.YAML_DUMP_KWARGS) diff --git a/pre_commit/git.py b/pre_commit/git.py index c38b83ab0..4fb2e65a1 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -97,7 +97,7 @@ def get_changed_files(new, old): )[1]) -def head_sha(remote): +def head_rev(remote): _, out, _ = cmd_output('git', 'ls-remote', '--exit-code', remote, 'HEAD') return out.split()[0] diff --git a/pre_commit/main.py b/pre_commit/main.py index e2f48ed34..92677147b 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -200,9 +200,9 @@ def main(argv=None): 'repo', help='Repository to source hooks from.', ) try_repo_parser.add_argument( - '--ref', + '--ref', '--rev', help=( - 'Manually select a ref to run against, otherwise the `HEAD` ' + 'Manually select a rev to run against, otherwise the `HEAD` ' 'revision will be used.' ), ) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 624ccd003..0647d9df7 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -150,8 +150,8 @@ def create(cls, config, store): @cached_property def manifest_hooks(self): - repo, sha = self.repo_config['repo'], self.repo_config['sha'] - repo_path = self.store.clone(repo, sha) + repo, rev = self.repo_config['repo'], self.repo_config['rev'] + repo_path = self.store.clone(repo, rev) manifest_path = os.path.join(repo_path, C.MANIFEST_FILE) return {hook['id']: hook for hook in load_manifest(manifest_path)} @@ -174,8 +174,8 @@ def hooks(self): ) def _prefix_from_deps(self, language_name, deps): - repo, sha = self.repo_config['repo'], self.repo_config['sha'] - return Prefix(self.store.clone(repo, sha, deps)) + repo, rev = self.repo_config['repo'], self.repo_config['rev'] + return Prefix(self.store.clone(repo, rev, deps)) def _venvs(self): ret = [] diff --git a/testing/fixtures.py b/testing/fixtures.py index 3537ca713..15c06df60 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -78,11 +78,11 @@ def config_with_local_hooks(): )) -def make_config_from_repo(repo_path, sha=None, hooks=None, check=True): +def make_config_from_repo(repo_path, rev=None, hooks=None, check=True): manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) config = OrderedDict(( ('repo', 'file://{}'.format(repo_path)), - ('sha', sha or git.head_sha(repo_path)), + ('rev', rev or git.head_rev(repo_path)), ( 'hooks', hooks or [OrderedDict((('id', hook['id']),)) for hook in manifest], diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 2f0b6fcb3..fcd34dc01 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -8,6 +8,7 @@ from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import is_local_repo from pre_commit.clientlib import MANIFEST_SCHEMA +from pre_commit.clientlib import MigrateShaToRev from pre_commit.clientlib import validate_config_main from pre_commit.clientlib import validate_manifest_main from testing.util import get_resource_path @@ -49,7 +50,7 @@ def test_validate_config_main(args, expected_output): ( {'repos': [{ 'repo': 'git@github.com:pre-commit/pre-commit-hooks', - 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', + 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', 'hooks': [{'id': 'pyflakes', 'files': '\\.py$'}], }]}, True, @@ -57,7 +58,7 @@ def test_validate_config_main(args, expected_output): ( {'repos': [{ 'repo': 'git@github.com:pre-commit/pre-commit-hooks', - 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', + 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', 'hooks': [ { 'id': 'pyflakes', @@ -71,7 +72,7 @@ def test_validate_config_main(args, expected_output): ( {'repos': [{ 'repo': 'git@github.com:pre-commit/pre-commit-hooks', - 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', + 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', 'hooks': [ { 'id': 'pyflakes', @@ -94,7 +95,7 @@ def test_config_valid(config_obj, expected): def test_config_with_local_hooks_definition_fails(): config_obj = {'repos': [{ 'repo': 'local', - 'sha': 'foo', + 'rev': 'foo', 'hooks': [{ 'id': 'do_not_commit', 'name': 'Block if "DO NOT COMMIT" is found', @@ -201,3 +202,45 @@ def test_validate_manifest_main(args, expected_output): def test_valid_manifests(manifest_obj, expected): ret = is_valid_according_to_schema(manifest_obj, MANIFEST_SCHEMA) assert ret is expected + + +@pytest.mark.parametrize( + 'dct', + ( + {'repo': 'local'}, {'repo': 'meta'}, + {'repo': 'wat', 'sha': 'wat'}, {'repo': 'wat', 'rev': 'wat'}, + ), +) +def test_migrate_sha_to_rev_ok(dct): + MigrateShaToRev().check(dct) + + +def test_migrate_sha_to_rev_dont_specify_both(): + with pytest.raises(cfgv.ValidationError) as excinfo: + MigrateShaToRev().check({'repo': 'a', 'sha': 'b', 'rev': 'c'}) + msg, = excinfo.value.args + assert msg == 'Cannot specify both sha and rev' + + +@pytest.mark.parametrize( + 'dct', + ( + {'repo': 'a'}, + {'repo': 'meta', 'sha': 'a'}, {'repo': 'meta', 'rev': 'a'}, + ), +) +def test_migrate_sha_to_rev_conditional_check_failures(dct): + with pytest.raises(cfgv.ValidationError): + MigrateShaToRev().check(dct) + + +def test_migrate_to_sha_apply_default(): + dct = {'repo': 'a', 'sha': 'b'} + MigrateShaToRev().apply_default(dct) + assert dct == {'repo': 'a', 'rev': 'b'} + + +def test_migrate_to_sha_ok(): + dct = {'repo': 'a', 'rev': 'b'} + MigrateShaToRev().apply_default(dct) + assert dct == {'repo': 'a', 'rev': 'b'} diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index e7fa66622..0c6ffbaca 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -32,9 +32,9 @@ def up_to_date_repo(tempdir_factory): def test_up_to_date_repo(up_to_date_repo, runner_with_mocked_store): config = make_config_from_repo(up_to_date_repo) - input_sha = config['sha'] + input_rev = config['rev'] ret = _update_repo(config, runner_with_mocked_store, tags_only=False) - assert ret['sha'] == input_sha + assert ret['rev'] == input_rev def test_autoupdate_up_to_date_repo( @@ -65,12 +65,12 @@ def test_autoupdate_old_revision_broken( cmd_output('git', '-C', path, 'mv', C.MANIFEST_FILE, 'nope.yaml') cmd_output('git', '-C', path, 'commit', '-m', 'simulate old repo') # Assume this is the revision the user's old repository was at - rev = git.head_sha(path) + rev = git.head_rev(path) cmd_output('git', '-C', path, 'mv', 'nope.yaml', C.MANIFEST_FILE) cmd_output('git', '-C', path, 'commit', '-m', 'move hooks file') - update_rev = git.head_sha(path) + update_rev = git.head_rev(path) - config['sha'] = rev + config['rev'] = rev write_config('.', config) before = open(C.CONFIG_FILE).read() ret = autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) @@ -83,24 +83,24 @@ def test_autoupdate_old_revision_broken( @pytest.fixture def out_of_date_repo(tempdir_factory): path = make_repo(tempdir_factory, 'python_hooks_repo') - original_sha = git.head_sha(path) + original_rev = git.head_rev(path) # Make a commit cmd_output('git', '-C', path, 'commit', '--allow-empty', '-m', 'foo') - head_sha = git.head_sha(path) + head_rev = git.head_rev(path) yield auto_namedtuple( - path=path, original_sha=original_sha, head_sha=head_sha, + path=path, original_rev=original_rev, head_rev=head_rev, ) def test_out_of_date_repo(out_of_date_repo, runner_with_mocked_store): config = make_config_from_repo( - out_of_date_repo.path, sha=out_of_date_repo.original_sha, + out_of_date_repo.path, rev=out_of_date_repo.original_rev, ) ret = _update_repo(config, runner_with_mocked_store, tags_only=False) - assert ret['sha'] != out_of_date_repo.original_sha - assert ret['sha'] == out_of_date_repo.head_sha + assert ret['rev'] != out_of_date_repo.original_rev + assert ret['rev'] == out_of_date_repo.head_rev def test_autoupdate_out_of_date_repo( @@ -108,7 +108,7 @@ def test_autoupdate_out_of_date_repo( ): # Write out the config config = make_config_from_repo( - out_of_date_repo.path, sha=out_of_date_repo.original_sha, check=False, + out_of_date_repo.path, rev=out_of_date_repo.original_rev, check=False, ) write_config('.', config) @@ -119,14 +119,14 @@ def test_autoupdate_out_of_date_repo( assert before != after # Make sure we don't add defaults assert 'exclude' not in after - assert out_of_date_repo.head_sha in after + assert out_of_date_repo.head_rev in after def test_autoupdate_out_of_date_repo_with_correct_repo_name( out_of_date_repo, in_tmpdir, mock_out_store_directory, ): stale_config = make_config_from_repo( - out_of_date_repo.path, sha=out_of_date_repo.original_sha, check=False, + out_of_date_repo.path, rev=out_of_date_repo.original_rev, check=False, ) local_config = config_with_local_hooks() config = {'repos': [stale_config, local_config]} @@ -140,7 +140,7 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( after = open(C.CONFIG_FILE).read() assert ret == 0 assert before != after - assert out_of_date_repo.head_sha in after + assert out_of_date_repo.head_rev in after assert local_config['repo'] in after @@ -149,7 +149,7 @@ def test_autoupdate_out_of_date_repo_with_wrong_repo_name( ): # Write out the config config = make_config_from_repo( - out_of_date_repo.path, sha=out_of_date_repo.original_sha, check=False, + out_of_date_repo.path, rev=out_of_date_repo.original_rev, check=False, ) write_config('.', config) @@ -168,40 +168,40 @@ def test_does_not_reformat( fmt = ( 'repos:\n' '- repo: {}\n' - ' sha: {} # definitely the version I want!\n' + ' rev: {} # definitely the version I want!\n' ' hooks:\n' ' - id: foo\n' ' # These args are because reasons!\n' ' args: [foo, bar, baz]\n' ) - config = fmt.format(out_of_date_repo.path, out_of_date_repo.original_sha) + config = fmt.format(out_of_date_repo.path, out_of_date_repo.original_rev) with open(C.CONFIG_FILE, 'w') as f: f.write(config) autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) after = open(C.CONFIG_FILE).read() - expected = fmt.format(out_of_date_repo.path, out_of_date_repo.head_sha) + expected = fmt.format(out_of_date_repo.path, out_of_date_repo.head_rev) assert after == expected def test_loses_formatting_when_not_detectable( out_of_date_repo, mock_out_store_directory, in_tmpdir, ): - """A best-effort attempt is made at updating sha without rewriting + """A best-effort attempt is made at updating rev without rewriting formatting. When the original formatting cannot be detected, this is abandoned. """ config = ( 'repos: [\n' ' {{\n' - ' repo: {}, sha: {},\n' + ' repo: {}, rev: {},\n' ' hooks: [\n' ' # A comment!\n' ' {{id: foo}},\n' ' ],\n' ' }}\n' ']\n'.format( - pipes.quote(out_of_date_repo.path), out_of_date_repo.original_sha, + pipes.quote(out_of_date_repo.path), out_of_date_repo.original_rev, ) ) with open(C.CONFIG_FILE, 'w') as f: @@ -212,10 +212,10 @@ def test_loses_formatting_when_not_detectable( expected = ( 'repos:\n' '- repo: {}\n' - ' sha: {}\n' + ' rev: {}\n' ' hooks:\n' ' - id: foo\n' - ).format(out_of_date_repo.path, out_of_date_repo.head_sha) + ).format(out_of_date_repo.path, out_of_date_repo.head_rev) assert after == expected @@ -229,7 +229,7 @@ def test_autoupdate_tagged_repo( tagged_repo, in_tmpdir, mock_out_store_directory, ): config = make_config_from_repo( - tagged_repo.path, sha=tagged_repo.original_sha, + tagged_repo.path, rev=tagged_repo.original_rev, ) write_config('.', config) @@ -250,7 +250,7 @@ def test_autoupdate_tags_only( ): config = make_config_from_repo( tagged_repo_with_more_commits.path, - sha=tagged_repo_with_more_commits.original_sha, + rev=tagged_repo_with_more_commits.original_rev, ) write_config('.', config) @@ -262,7 +262,7 @@ def test_autoupdate_tags_only( @pytest.fixture def hook_disappearing_repo(tempdir_factory): path = make_repo(tempdir_factory, 'python_hooks_repo') - original_sha = git.head_sha(path) + original_rev = git.head_rev(path) shutil.copy( get_resource_path('manifest_without_foo.yaml'), @@ -271,7 +271,7 @@ def hook_disappearing_repo(tempdir_factory): cmd_output('git', '-C', path, 'add', '.') cmd_output('git', '-C', path, 'commit', '-m', 'Remove foo') - yield auto_namedtuple(path=path, original_sha=original_sha) + yield auto_namedtuple(path=path, original_rev=original_rev) def test_hook_disppearing_repo_raises( @@ -279,7 +279,7 @@ def test_hook_disppearing_repo_raises( ): config = make_config_from_repo( hook_disappearing_repo.path, - sha=hook_disappearing_repo.original_sha, + rev=hook_disappearing_repo.original_rev, hooks=[OrderedDict((('id', 'foo'),))], ) with pytest.raises(RepositoryCannotBeUpdatedError): @@ -291,7 +291,7 @@ def test_autoupdate_hook_disappearing_repo( ): config = make_config_from_repo( hook_disappearing_repo.path, - sha=hook_disappearing_repo.original_sha, + rev=hook_disappearing_repo.original_rev, hooks=[OrderedDict((('id', 'foo'),))], check=False, ) @@ -319,7 +319,7 @@ def test_autoupdate_local_hooks_with_out_of_date_repo( out_of_date_repo, in_tmpdir, mock_out_store_directory, ): stale_config = make_config_from_repo( - out_of_date_repo.path, sha=out_of_date_repo.original_sha, check=False, + out_of_date_repo.path, rev=out_of_date_repo.original_rev, check=False, ) local_config = config_with_local_hooks() config = {'repos': [local_config, stale_config]} diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index 7b43098bf..a2a34b665 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -118,3 +118,30 @@ def test_already_migrated_configuration_noop(tmpdir, capsys): out, _ = capsys.readouterr() assert out == 'Configuration is already migrated.\n' assert cfg.read() == contents + + +def test_migrate_config_sha_to_rev(tmpdir): + contents = ( + 'repos:\n' + '- repo: https://github.com/pre-commit/pre-commit-hooks\n' + ' sha: v1.2.0\n' + ' hooks: []\n' + 'repos:\n' + '- repo: https://github.com/pre-commit/pre-commit-hooks\n' + ' sha: v1.2.0\n' + ' hooks: []\n' + ) + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write(contents) + assert not migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) + contents = cfg.read() + assert contents == ( + 'repos:\n' + '- repo: https://github.com/pre-commit/pre-commit-hooks\n' + ' rev: v1.2.0\n' + ' hooks: []\n' + 'repos:\n' + '- repo: https://github.com/pre-commit/pre-commit-hooks\n' + ' rev: v1.2.0\n' + ' hooks: []\n' + ) diff --git a/tests/commands/sample_config_test.py b/tests/commands/sample_config_test.py index 1dca98b4c..7c4e88d88 100644 --- a/tests/commands/sample_config_test.py +++ b/tests/commands/sample_config_test.py @@ -13,7 +13,7 @@ def test_sample_config(capsys): # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - sha: v0.9.2 + rev: v1.2.1-1 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index a29181b83..4fb0755c2 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -39,7 +39,7 @@ def test_try_repo_repo_only(cap_out, tempdir_factory): assert re.match( '^repos:\n' '- repo: .+\n' - ' sha: .+\n' + ' rev: .+\n' ' hooks:\n' ' - id: bash_hook\n' ' - id: bash_hook2\n' @@ -63,7 +63,7 @@ def test_try_repo_with_specific_hook(cap_out, tempdir_factory): assert re.match( '^repos:\n' '- repo: .+\n' - ' sha: .+\n' + ' rev: .+\n' ' hooks:\n' ' - id: bash_hook\n$', config, diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py index 414f853c1..65715acd0 100644 --- a/tests/make_archives_test.py +++ b/tests/make_archives_test.py @@ -19,8 +19,8 @@ def test_make_archive(tempdir_factory): open(os.path.join(git_path, 'foo'), 'a').close() cmd_output('git', '-C', git_path, 'add', '.') cmd_output('git', '-C', git_path, 'commit', '-m', 'foo') - # We'll use this sha - head_sha = git.head_sha(git_path) + # We'll use this rev + head_rev = git.head_rev(git_path) # And check that this file doesn't exist open(os.path.join(git_path, 'bar'), 'a').close() cmd_output('git', '-C', git_path, 'add', '.') @@ -28,7 +28,7 @@ def test_make_archive(tempdir_factory): # Do the thing archive_path = make_archives.make_archive( - 'foo', git_path, head_sha, output_dir, + 'foo', git_path, head_rev, output_dir, ) assert archive_path == os.path.join(output_dir, 'foo.tar.gz') diff --git a/tests/repository_test.py b/tests/repository_test.py index 7f0593bfd..63b9f1c9c 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -660,14 +660,14 @@ def test_tags_on_repositories(in_tmpdir, tempdir_factory, store): ) repo_1 = Repository.create( - make_config_from_repo(git_dir_1, sha=tag), store, + make_config_from_repo(git_dir_1, rev=tag), store, ) ret = repo_1.run_hook(repo_1.hooks[0][1], ['-L']) assert ret[0] == 0 assert ret[1].strip() == _norm_pwd(in_tmpdir) repo_2 = Repository.create( - make_config_from_repo(git_dir_2, sha=tag), store, + make_config_from_repo(git_dir_2, rev=tag), store, ) ret = repo_2.run_hook(repo_2.hooks[0][1], ['bar']) assert ret[0] == 0 diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 481a2886a..932ee4b6f 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -192,15 +192,14 @@ def submodule_with_commits(tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): cmd_output('git', 'commit', '--allow-empty', '-m', 'foo') - sha1 = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() + rev1 = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() cmd_output('git', 'commit', '--allow-empty', '-m', 'bar') - sha2 = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() - yield auto_namedtuple(path=path, sha1=sha1, sha2=sha2) + rev2 = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() + yield auto_namedtuple(path=path, rev1=rev1, rev2=rev2) -def checkout_submodule(sha): - with cwd('sub'): - cmd_output('git', 'checkout', sha) +def checkout_submodule(rev): + cmd_output('git', '-C', 'sub', 'checkout', rev) @pytest.fixture @@ -210,7 +209,7 @@ def sub_staged(submodule_with_commits, tempdir_factory): cmd_output( 'git', 'submodule', 'add', submodule_with_commits.path, 'sub', ) - checkout_submodule(submodule_with_commits.sha1) + checkout_submodule(submodule_with_commits.rev1) cmd_output('git', 'add', 'sub') yield auto_namedtuple( path=path, @@ -219,11 +218,11 @@ def sub_staged(submodule_with_commits, tempdir_factory): ) -def _test_sub_state(path, sha='sha1', status='A'): +def _test_sub_state(path, rev='rev1', status='A'): assert os.path.exists(path.sub_path) with cwd(path.sub_path): - actual_sha = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() - assert actual_sha == getattr(path.submodule, sha) + actual_rev = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() + assert actual_rev == getattr(path.submodule, rev) actual_status = get_short_git_status()['sub'] assert actual_status == status @@ -239,15 +238,15 @@ def test_sub_nothing_unstaged(sub_staged, patch_dir): def test_sub_something_unstaged(sub_staged, patch_dir): - checkout_submodule(sub_staged.submodule.sha2) + checkout_submodule(sub_staged.submodule.rev2) - _test_sub_state(sub_staged, 'sha2', 'AM') + _test_sub_state(sub_staged, 'rev2', 'AM') with staged_files_only(patch_dir): # This is different from others, we don't want to touch subs - _test_sub_state(sub_staged, 'sha2', 'AM') + _test_sub_state(sub_staged, 'rev2', 'AM') - _test_sub_state(sub_staged, 'sha2', 'AM') + _test_sub_state(sub_staged, 'rev2', 'AM') def test_stage_utf8_changes(foo_staged, patch_dir): diff --git a/tests/store_test.py b/tests/store_test.py index 86c3ec44c..4e80f0592 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -91,10 +91,10 @@ def test_clone(store, tempdir_factory, log_info_mock): path = git_dir(tempdir_factory) with cwd(path): cmd_output('git', 'commit', '--allow-empty', '-m', 'foo') - sha = git.head_sha(path) + rev = git.head_rev(path) cmd_output('git', 'commit', '--allow-empty', '-m', 'bar') - ret = store.clone(path, sha) + ret = store.clone(path, rev) # Should have printed some stuff assert log_info_mock.call_args_list[0][0][0].startswith( 'Initializing environment for ', @@ -106,14 +106,14 @@ def test_clone(store, tempdir_factory, log_info_mock): # Directory should start with `repo` _, dirname = os.path.split(ret) assert dirname.startswith('repo') - # Should be checked out to the sha we specified - assert git.head_sha(ret) == sha + # Should be checked out to the rev we specified + assert git.head_rev(ret) == rev # Assert there's an entry in the sqlite db for this with sqlite3.connect(store.db_path) as db: path, = db.execute( 'SELECT path from repos WHERE repo = ? and ref = ?', - [path, sha], + (path, rev), ).fetchone() assert path == ret @@ -122,7 +122,7 @@ def test_clone_cleans_up_on_checkout_failure(store): try: # This raises an exception because you can't clone something that # doesn't exist! - store.clone('/i_dont_exist_lol', 'fake_sha') + store.clone('/i_dont_exist_lol', 'fake_rev') except Exception as e: assert '/i_dont_exist_lol' in six.text_type(e) From 69333fa2277deaf30be41f7b04a196b0b5a8b101 Mon Sep 17 00:00:00 2001 From: Thierry Deo Date: Mon, 26 Feb 2018 10:17:21 +0100 Subject: [PATCH 159/544] Add multiline mode to pygrep --- pre_commit/languages/pygrep.py | 15 ++++++++++++++- tests/languages/pygrep_test.py | 6 ++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 878f57d06..34d77da14 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -26,6 +26,15 @@ def _process_filename_by_line(pattern, filename): output.write_line(line.rstrip(b'\r\n')) return retv +def _process_filename_at_once(pattern, filename): + retv = 0 + with open(filename, 'rb') as f: + match = pattern.search(f.read()) + if match: + retv = 1 + output.write('{}:'.format(filename)) + output.write_line(match.group()) + return retv def run_hook(prefix, hook, file_args): exe = (sys.executable, '-m', __name__) @@ -42,6 +51,7 @@ def main(argv=None): ), ) parser.add_argument('-i', '--ignore-case', action='store_true') + parser.add_argument('-z', '--null-data', action='store_true') parser.add_argument('pattern', help='python regex pattern.') parser.add_argument('filenames', nargs='*') args = parser.parse_args(argv) @@ -51,7 +61,10 @@ def main(argv=None): retv = 0 for filename in args.filenames: - retv |= _process_filename_by_line(pattern, filename) + if args.null_data: + retv |= _process_filename_at_once(pattern, filename) + else: + retv |= _process_filename_by_line(pattern, filename) return retv diff --git a/tests/languages/pygrep_test.py b/tests/languages/pygrep_test.py index 048a59088..ece454f97 100644 --- a/tests/languages/pygrep_test.py +++ b/tests/languages/pygrep_test.py @@ -38,3 +38,9 @@ def test_ignore_case(some_files, cap_out): out = cap_out.get() assert ret == 1 assert out == 'f2:1:[INFO] hi\n' + +def test_null_data(some_files, cap_out): + ret = pygrep.main(('--null-data', r'foo.*bar', 'f1', 'f2', 'f3')) + out = cap_out.get() + assert ret == 1 + assert out == 'f1:foobar\n' From 2d57068f498807fdf5c8a36bdadbe59beb9d2a62 Mon Sep 17 00:00:00 2001 From: Thierry Deo Date: Mon, 26 Feb 2018 13:29:40 +0100 Subject: [PATCH 160/544] Remove newlines from file contents --- pre_commit/languages/pygrep.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 34d77da14..0447837ab 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -29,7 +29,7 @@ def _process_filename_by_line(pattern, filename): def _process_filename_at_once(pattern, filename): retv = 0 with open(filename, 'rb') as f: - match = pattern.search(f.read()) + match = pattern.search(f.read().decode('utf-8').replace('\n','')) if match: retv = 1 output.write('{}:'.format(filename)) From 3793bc32c039550014bf3646a6e78e11ded35c89 Mon Sep 17 00:00:00 2001 From: Thierry Deo Date: Mon, 26 Feb 2018 15:46:33 +0100 Subject: [PATCH 161/544] Fix linters --- pre_commit/languages/pygrep.py | 4 +++- tests/languages/pygrep_test.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 0447837ab..bc01208af 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -26,16 +26,18 @@ def _process_filename_by_line(pattern, filename): output.write_line(line.rstrip(b'\r\n')) return retv + def _process_filename_at_once(pattern, filename): retv = 0 with open(filename, 'rb') as f: - match = pattern.search(f.read().decode('utf-8').replace('\n','')) + match = pattern.search(f.read().decode('utf-8').replace('\n', '')) if match: retv = 1 output.write('{}:'.format(filename)) output.write_line(match.group()) return retv + def run_hook(prefix, hook, file_args): exe = (sys.executable, '-m', __name__) exe += tuple(hook['args']) + (hook['entry'],) diff --git a/tests/languages/pygrep_test.py b/tests/languages/pygrep_test.py index ece454f97..33250e4a2 100644 --- a/tests/languages/pygrep_test.py +++ b/tests/languages/pygrep_test.py @@ -39,6 +39,7 @@ def test_ignore_case(some_files, cap_out): assert ret == 1 assert out == 'f2:1:[INFO] hi\n' + def test_null_data(some_files, cap_out): ret = pygrep.main(('--null-data', r'foo.*bar', 'f1', 'f2', 'f3')) out = cap_out.get() From 2722e16fd86a611da82058056924e7940219b0ee Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 1 Mar 2018 14:06:48 -0800 Subject: [PATCH 162/544] Use --clean-src for nodeenv --- pre_commit/languages/node.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 4779db509..7b4649302 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -55,7 +55,9 @@ def install_environment(prefix, version, additional_dependencies): if sys.platform == 'win32': # pragma: no cover envdir = '\\\\?\\' + os.path.normpath(envdir) with clean_path_on_failure(envdir): - cmd = [sys.executable, '-m', 'nodeenv', '--prebuilt', envdir] + cmd = [ + sys.executable, '-mnodeenv', '--prebuilt', '--clean-src', envdir, + ] if version != 'default': cmd.extend(['-n', version]) cmd_output(*cmd) From f679983012e4947bfa2cfbf0aa74c1df9452f5ee Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 3 Mar 2018 14:42:20 -0800 Subject: [PATCH 163/544] Refuse to install with core.hooksPath set --- pre_commit/commands/install_uninstall.py | 11 +++++++++++ tests/commands/install_uninstall_test.py | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 83b97cb1e..919122265 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -2,15 +2,19 @@ from __future__ import unicode_literals import io +import logging import os.path import sys from pre_commit import output +from pre_commit.util import cmd_output from pre_commit.util import make_executable from pre_commit.util import mkdirp from pre_commit.util import resource_filename +logger = logging.getLogger(__name__) + # This is used to identify the hook file we install PRIOR_HASHES = ( '4d9958c90bc262f47553e2c073f14cfe', @@ -36,6 +40,13 @@ def install( skip_on_missing_conf=False, ): """Install the pre-commit hooks.""" + if cmd_output('git', 'config', 'core.hooksPath', retcode=None)[1].strip(): + logger.error( + 'Cowardly refusing to install hooks with `core.hooksPath` set.\n' + 'hint: `git config --unset-all core.hooksPath`', + ) + return 1 + hook_path = runner.get_hook_path(hook_type) legacy_path = hook_path + '.legacy' diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index a49a3e4f3..f83708ea8 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -66,6 +66,14 @@ def test_install_hooks_directory_not_present(tempdir_factory): assert os.path.exists(runner.pre_commit_path) +def test_install_refuses_core_hookspath(tempdir_factory): + path = git_dir(tempdir_factory) + with cwd(path): + cmd_output('git', 'config', '--local', 'core.hooksPath', 'hooks') + runner = Runner(path, C.CONFIG_FILE) + assert install(runner) + + @xfailif_no_symlink def test_install_hooks_dead_symlink( tempdir_factory, From f76e7b8eb6ed853676c58044dfb904137d16a2b7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 3 Mar 2018 16:31:58 -0800 Subject: [PATCH 164/544] v1.7.0 --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a60566859..f2b91b33b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,37 @@ +1.7.0 +===== + +### Features +- pre-commit config validation was split to a separate `cfgv` library + - #700 PR by @asottile. +- Allow `--repo` to be specified multiple times to autoupdate + - #658 issue by @KevinHock. + - #713 PR by @asottile. +- Enable `rev` as a preferred alternative to `sha` in `.pre-commit-config.yaml` + - #106 issue by @asottile. + - #715 PR by @asottile. +- Use `--clean-src` option when invoking `nodeenv` to save ~70MB per node env + - #717 PR by @asottile. +- Refuse to install with `core.hooksPath` set + - pre-commit/pre-commit-hooks#250 issue by @revolter. + - #663 issue by @asottile. + - #718 PR by @asottile. + +### Fixes +- hooks with `additional_dependencies` now get isolated environments + - #590 issue by @coldnight. + - #711 PR by @asottile. + +### Misc +- test against swift 4.x + - #709 by @theresama. + +### Updating + +- Run `pre-commit migrate-config` to convert `sha` to `rev` in the + `.pre-commit-config.yaml` file. + + 1.6.0 ===== diff --git a/setup.py b/setup.py index 99c5f44d9..d6d2d3301 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.6.0', + version='1.7.0', author='Anthony Sottile', author_email='asottile@umich.edu', From 25c06e65259f858e099f995f2f302bf1e8ff9efb Mon Sep 17 00:00:00 2001 From: Thierry Deo Date: Wed, 7 Mar 2018 09:24:56 +0100 Subject: [PATCH 165/544] Remove encoding dependence --- pre_commit/languages/pygrep.py | 7 +++++-- tests/languages/pygrep_test.py | 18 ++++++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index bc01208af..b1af2f20f 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -30,10 +30,10 @@ def _process_filename_by_line(pattern, filename): def _process_filename_at_once(pattern, filename): retv = 0 with open(filename, 'rb') as f: - match = pattern.search(f.read().decode('utf-8').replace('\n', '')) + match = pattern.search(f.read()) if match: retv = 1 - output.write('{}:'.format(filename)) + output.write('{}:{}-{}:'.format(filename, match.start(), match.end())) output.write_line(match.group()) return retv @@ -59,6 +59,9 @@ def main(argv=None): args = parser.parse_args(argv) flags = re.IGNORECASE if args.ignore_case else 0 + if args.null_data: + flags = flags | re.MULTILINE | re.DOTALL + pattern = re.compile(args.pattern.encode(), flags) retv = 0 diff --git a/tests/languages/pygrep_test.py b/tests/languages/pygrep_test.py index 33250e4a2..e2063a953 100644 --- a/tests/languages/pygrep_test.py +++ b/tests/languages/pygrep_test.py @@ -41,7 +41,21 @@ def test_ignore_case(some_files, cap_out): def test_null_data(some_files, cap_out): - ret = pygrep.main(('--null-data', r'foo.*bar', 'f1', 'f2', 'f3')) + ret = pygrep.main(('--null-data', r'foo\nbar', 'f1', 'f2', 'f3')) out = cap_out.get() assert ret == 1 - assert out == 'f1:foobar\n' + assert out == 'f1:0-7:foo\nbar\n' + + +def test_null_data_dotall_flag_is_enabled(some_files, cap_out): + ret = pygrep.main(('--null-data', r'o.*bar', 'f1', 'f2', 'f3')) + out = cap_out.get() + assert ret == 1 + assert out == 'f1:1-7:oo\nbar\n' + + +def test_null_data_multiline_flag_is_enabled(some_files, cap_out): + ret = pygrep.main(('--null-data', r'foo$.*bar', 'f1', 'f2', 'f3')) + out = cap_out.get() + assert ret == 1 + assert out == 'f1:0-7:foo\nbar\n' From 19075371fa1770fc8f29e407111a275046c617ab Mon Sep 17 00:00:00 2001 From: Thierry Deo Date: Wed, 7 Mar 2018 09:35:08 +0100 Subject: [PATCH 166/544] Pre-commit compliance --- pre_commit/languages/pygrep.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index b1af2f20f..36755dd7f 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -33,7 +33,9 @@ def _process_filename_at_once(pattern, filename): match = pattern.search(f.read()) if match: retv = 1 - output.write('{}:{}-{}:'.format(filename, match.start(), match.end())) + output.write( + '{}:{}-{}:'.format(filename, match.start(), match.end()), + ) output.write_line(match.group()) return retv From 4088f55ee6919e2566c4573512fa3c745199cff0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 7 Mar 2018 12:18:54 -0800 Subject: [PATCH 167/544] Don't need a shell here --- tests/commands/run_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 8107e79ab..931ad4a11 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -518,7 +518,7 @@ def test_lots_of_files(mock_out_store_directory, tempdir_factory): filename = '{}{}'.format('a' * 100, i) open(filename, 'w').close() - cmd_output('bash', '-c', 'git add .') + cmd_output('git', 'add', '.') install(Runner(git_path, C.CONFIG_FILE)) cmd_output_mocked_pre_commit_home( From bf5792eb10a950b6547393b1dbfb19b9477c8e82 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 7 Mar 2018 12:41:25 -0800 Subject: [PATCH 168/544] Add a manual stage for cli-only interaction --- pre_commit/clientlib.py | 7 ++--- pre_commit/constants.py | 3 ++ pre_commit/main.py | 5 ++-- tests/commands/run_test.py | 59 ++++++++++++++------------------------ tests/conftest.py | 3 +- 5 files changed, 31 insertions(+), 46 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index f6f86191e..4570e1079 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -35,10 +35,7 @@ def _make_argparser(filenames_help): cfgv.Required('id', cfgv.check_string), cfgv.Required('name', cfgv.check_string), cfgv.Required('entry', cfgv.check_string), - cfgv.Required( - 'language', - cfgv.check_and(cfgv.check_string, cfgv.check_one_of(all_languages)), - ), + cfgv.Required('language', cfgv.check_one_of(all_languages)), cfgv.Optional( 'files', cfgv.check_and(cfgv.check_string, cfgv.check_regex), '', @@ -59,7 +56,7 @@ def _make_argparser(filenames_help): cfgv.Optional('language_version', cfgv.check_string, 'default'), cfgv.Optional('log_file', cfgv.check_string, ''), cfgv.Optional('minimum_pre_commit_version', cfgv.check_string, '0'), - cfgv.Optional('stages', cfgv.check_array(cfgv.check_string), []), + cfgv.Optional('stages', cfgv.check_array(cfgv.check_one_of(C.STAGES)), []), cfgv.Optional('verbose', cfgv.check_bool, False), ) MANIFEST_SCHEMA = cfgv.Array(MANIFEST_HOOK_DICT) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 2fa435522..48ba2cb9c 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -20,3 +20,6 @@ VERSION = pkg_resources.get_distribution('pre-commit').version VERSION_PARSED = pkg_resources.parse_version(VERSION) + +# `manual` is not invoked by any installed git hook. See #719 +STAGES = ('commit', 'commit-msg', 'manual', 'push') diff --git a/pre_commit/main.py b/pre_commit/main.py index 92677147b..18db533a8 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -70,9 +70,8 @@ def _add_run_options(parser): help='Filename to check when running during `commit-msg`', ) parser.add_argument( - '--hook-stage', choices=('commit', 'push', 'commit-msg'), - default='commit', - help='The stage during which the hook is fired e.g. commit or push.', + '--hook-stage', choices=C.STAGES, default='commit', + help='The stage during which the hook is fired. One of %(choices)s', ) parser.add_argument( '--show-diff-on-failure', action='store_true', diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 931ad4a11..4df65117c 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -529,52 +529,37 @@ def test_lots_of_files(mock_out_store_directory, tempdir_factory): ) -def test_push_hook(cap_out, repo_with_passing_hook, mock_out_store_directory): +def test_stages(cap_out, repo_with_passing_hook, mock_out_store_directory): config = OrderedDict(( ('repo', 'local'), ( - 'hooks', ( - OrderedDict(( - ('id', 'flake8'), - ('name', 'hook 1'), - ('entry', "'{}' -m flake8".format(sys.executable)), - ('language', 'system'), - ('types', ['python']), - ('stages', ['commit']), - )), - OrderedDict(( - ('id', 'do_not_commit'), - ('name', 'hook 2'), - ('entry', 'DO NOT COMMIT'), - ('language', 'pygrep'), - ('types', ['text']), - ('stages', ['push']), - )), + 'hooks', tuple( + { + 'id': 'do-not-commit-{}'.format(i), + 'name': 'hook {}'.format(i), + 'entry': 'DO NOT COMMIT', + 'language': 'pygrep', + 'stages': [stage], + } + for i, stage in enumerate(('commit', 'push', 'manual'), 1) ), ), )) add_config_to_repo(repo_with_passing_hook, config) - open('dummy.py', 'a').close() - cmd_output('git', 'add', 'dummy.py') - - _test_run( - cap_out, - repo_with_passing_hook, - {'hook_stage': 'commit'}, - expected_outputs=[b'hook 1'], - expected_ret=0, - stage=False, - ) + stage_a_file() - _test_run( - cap_out, - repo_with_passing_hook, - {'hook_stage': 'push'}, - expected_outputs=[b'hook 2'], - expected_ret=0, - stage=False, - ) + def _run_for_stage(stage): + args = run_opts(hook_stage=stage) + ret, printed = _do_run(cap_out, repo_with_passing_hook, args) + assert not ret, (ret, printed) + # this test should only run one hook + assert printed.count(b'hook ') == 1 + return printed + + assert _run_for_stage('commit').startswith(b'hook 1...') + assert _run_for_stage('push').startswith(b'hook 2...') + assert _run_for_stage('manual').startswith(b'hook 3...') def test_commit_msg_hook(cap_out, commit_msg_repo, mock_out_store_directory): diff --git a/tests/conftest.py b/tests/conftest.py index 678010f50..2d27a4a42 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -180,7 +180,8 @@ def __init__(self, stream): def get_bytes(self): """Get the output as-if no encoding occurred""" data = self._stream.data.getvalue() - self._stream.data.truncate(0) + self._stream.data.seek(0) + self._stream.data.truncate() return data def get(self): From d760c794a6a50ea88cddef1552e7db91b1b7209c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 7 Mar 2018 17:43:33 -0800 Subject: [PATCH 169/544] Normalize git urls in pre-commit config Improves cache performance Committed via https://github.com/asottile/all-repos --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e4888a0b9..b5da1b169 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ repos: -- repo: https://github.com/pre-commit/pre-commit-hooks.git +- repo: https://github.com/pre-commit/pre-commit-hooks sha: v0.9.1 hooks: - id: trailing-whitespace @@ -12,11 +12,11 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - id: flake8 -- repo: https://github.com/pre-commit/pre-commit.git +- repo: https://github.com/pre-commit/pre-commit rev: v0.16.3 hooks: - id: validate_manifest -- repo: https://github.com/asottile/reorder_python_imports.git +- repo: https://github.com/asottile/reorder_python_imports rev: v0.3.5 hooks: - id: reorder-python-imports From 55c74c10d95af5719f771cd1db894a57fa801ea2 Mon Sep 17 00:00:00 2001 From: Thierry Deo Date: Thu, 8 Mar 2018 09:42:32 +0100 Subject: [PATCH 170/544] Rename option to and improve output --- pre_commit/languages/pygrep.py | 20 ++++++++++++++------ tests/languages/pygrep_test.py | 25 ++++++++++++++++--------- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 36755dd7f..a1d496b5e 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -30,13 +30,21 @@ def _process_filename_by_line(pattern, filename): def _process_filename_at_once(pattern, filename): retv = 0 with open(filename, 'rb') as f: - match = pattern.search(f.read()) + contents = f.read() + match = pattern.search(contents) if match: retv = 1 + line_no = len( + re.compile('\n'.encode()).findall(contents, 0, match.start()), + ) output.write( - '{}:{}-{}:'.format(filename, match.start(), match.end()), + '{}:{}:'.format(filename, line_no + 1), ) - output.write_line(match.group()) + + matched_lines = match.group().split('\n') + matched_lines[0] = contents.split('\n')[line_no] + + output.write_line('\n'.join(matched_lines)) return retv @@ -55,20 +63,20 @@ def main(argv=None): ), ) parser.add_argument('-i', '--ignore-case', action='store_true') - parser.add_argument('-z', '--null-data', action='store_true') + parser.add_argument('--multiline', action='store_true') parser.add_argument('pattern', help='python regex pattern.') parser.add_argument('filenames', nargs='*') args = parser.parse_args(argv) flags = re.IGNORECASE if args.ignore_case else 0 - if args.null_data: + if args.multiline: flags = flags | re.MULTILINE | re.DOTALL pattern = re.compile(args.pattern.encode(), flags) retv = 0 for filename in args.filenames: - if args.null_data: + if args.multiline: retv |= _process_filename_at_once(pattern, filename) else: retv |= _process_filename_by_line(pattern, filename) diff --git a/tests/languages/pygrep_test.py b/tests/languages/pygrep_test.py index e2063a953..d91363e2f 100644 --- a/tests/languages/pygrep_test.py +++ b/tests/languages/pygrep_test.py @@ -40,22 +40,29 @@ def test_ignore_case(some_files, cap_out): assert out == 'f2:1:[INFO] hi\n' -def test_null_data(some_files, cap_out): - ret = pygrep.main(('--null-data', r'foo\nbar', 'f1', 'f2', 'f3')) +def test_multiline(some_files, cap_out): + ret = pygrep.main(('--multiline', r'foo\nbar', 'f1', 'f2', 'f3')) out = cap_out.get() assert ret == 1 - assert out == 'f1:0-7:foo\nbar\n' + assert out == 'f1:1:foo\nbar\n' -def test_null_data_dotall_flag_is_enabled(some_files, cap_out): - ret = pygrep.main(('--null-data', r'o.*bar', 'f1', 'f2', 'f3')) +def test_multiline_line_number(some_files, cap_out): + ret = pygrep.main(('--multiline', r'ar', 'f1', 'f2', 'f3')) out = cap_out.get() assert ret == 1 - assert out == 'f1:1-7:oo\nbar\n' + assert out == 'f1:2:bar\n' -def test_null_data_multiline_flag_is_enabled(some_files, cap_out): - ret = pygrep.main(('--null-data', r'foo$.*bar', 'f1', 'f2', 'f3')) +def test_multiline_dotall_flag_is_enabled(some_files, cap_out): + ret = pygrep.main(('--multiline', r'o.*bar', 'f1', 'f2', 'f3')) out = cap_out.get() assert ret == 1 - assert out == 'f1:0-7:foo\nbar\n' + assert out == 'f1:1:foo\nbar\n' + + +def test_multiline_multiline_flag_is_enabled(some_files, cap_out): + ret = pygrep.main(('--multiline', r'foo$.*bar', 'f1', 'f2', 'f3')) + out = cap_out.get() + assert ret == 1 + assert out == 'f1:1:foo\nbar\n' From 67c49cd6a40c19247927b638ada115388402cf9b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 8 Mar 2018 22:41:50 -0800 Subject: [PATCH 171/544] Ran pre-commit autoupdate. Committed via https://github.com/asottile/all-repos --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b5da1b169..a146bd25b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - sha: v0.9.1 + rev: v1.2.3 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -13,11 +13,11 @@ repos: - id: requirements-txt-fixer - id: flake8 - repo: https://github.com/pre-commit/pre-commit - rev: v0.16.3 + rev: v1.7.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/reorder_python_imports - rev: v0.3.5 + rev: v1.0.1 hooks: - id: reorder-python-imports language_version: python2.7 From 55ef3ce96058b723c5f557aa16255b99e26f7ce9 Mon Sep 17 00:00:00 2001 From: Thierry Deo Date: Fri, 9 Mar 2018 09:22:34 +0100 Subject: [PATCH 172/544] Address review comments --- pre_commit/languages/pygrep.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index a1d496b5e..7eead9e1b 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -34,17 +34,13 @@ def _process_filename_at_once(pattern, filename): match = pattern.search(contents) if match: retv = 1 - line_no = len( - re.compile('\n'.encode()).findall(contents, 0, match.start()), - ) - output.write( - '{}:{}:'.format(filename, line_no + 1), - ) + line_no = contents[:match.start()].count(b'\n') + output.write('{}:{}:'.format(filename, line_no + 1)) - matched_lines = match.group().split('\n') - matched_lines[0] = contents.split('\n')[line_no] + matched_lines = match.group().split(b'\n') + matched_lines[0] = contents.split(b'\n')[line_no] - output.write_line('\n'.join(matched_lines)) + output.write_line(b'\n'.join(matched_lines)) return retv @@ -70,7 +66,7 @@ def main(argv=None): flags = re.IGNORECASE if args.ignore_case else 0 if args.multiline: - flags = flags | re.MULTILINE | re.DOTALL + flags |= re.MULTILINE | re.DOTALL pattern = re.compile(args.pattern.encode(), flags) From ae2eac5c082565359b4c6d67db26c302e2b1aee4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 11 Mar 2018 20:19:16 -0700 Subject: [PATCH 173/544] v1.8.0 --- CHANGELOG.md | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2b91b33b..5c242b2cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +1.8.0 +===== + +### Features +- Add a `manual` stage for cli-only interaction + - #719 issue by @hectorv. + - #720 PR by @asottile. +- Add a `--multiline` option to `pygrep` hooks + - #716 PR by @tdeo. + + 1.7.0 ===== diff --git a/setup.py b/setup.py index d6d2d3301..4e17559fd 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.7.0', + version='1.8.0', author='Anthony Sottile', author_email='asottile@umich.edu', From 65f001b0078f8385cb6e9723c5613198dd68080f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 12 Mar 2018 13:51:03 -0700 Subject: [PATCH 174/544] Fix go 1.10: no pkg dir --- pre_commit/languages/golang.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 35cfa2ad8..14354e0ce 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -74,7 +74,9 @@ def install_environment(prefix, version, additional_dependencies): cmd_output('go', 'get', dependency, cwd=repo_src_dir, env=env) # Same some disk space, we don't need these after installation rmtree(prefix.path(directory, 'src')) - rmtree(prefix.path(directory, 'pkg')) + pkgdir = prefix.path(directory, 'pkg') + if os.path.exists(pkgdir): # pragma: no cover (go<1.10) + rmtree(pkgdir) def run_hook(prefix, hook, file_args): From d9d5b1cef17ac5e5dd380c9db0e4d56ade207fb8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 12 Mar 2018 14:34:53 -0700 Subject: [PATCH 175/544] Fix typo --- tests/commands/run_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 4df65117c..d664e8013 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -811,7 +811,7 @@ def test_include_exclude_base_case(some_filenames): @xfailif_no_symlink -def test_matches_broken_symlink(tmpdir): # pramga: no cover (non-windows) +def test_matches_broken_symlink(tmpdir): # pragma: no cover (non-windows) with tmpdir.as_cwd(): os.symlink('does-not-exist', 'link') ret = _filter_by_include_exclude({'link'}, '', '^$') From 96e9d1b758d7b4084e1cfe473c866cb837f1bde8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 12 Mar 2018 13:09:05 -0700 Subject: [PATCH 176/544] Restore git 1.8 support --- latest-git.sh | 3 +-- pre_commit/commands/autoupdate.py | 10 +++++----- pre_commit/make_archives.py | 2 +- pre_commit/store.py | 4 ++-- testing/fixtures.py | 13 ++++++------- tests/commands/autoupdate_test.py | 19 +++++++++---------- tests/commands/install_uninstall_test.py | 4 ++-- tests/conftest.py | 8 ++++---- tests/make_archives_test.py | 8 ++++---- tests/staged_files_only_test.py | 2 +- 10 files changed, 35 insertions(+), 38 deletions(-) diff --git a/latest-git.sh b/latest-git.sh index 75c6f62a5..0f7a52a6b 100755 --- a/latest-git.sh +++ b/latest-git.sh @@ -3,6 +3,5 @@ set -ex git clone git://github.com/git/git --depth 1 /tmp/git pushd /tmp/git -make prefix=/tmp/git -j 8 all -make prefix=/tmp/git install +make prefix=/tmp/git -j8 install popd diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index cdaccfca2..f4ce67508 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -34,17 +34,17 @@ def _update_repo(repo_config, runner, tags_only): """ repo_path = runner.store.clone(repo_config['repo'], repo_config['rev']) - cmd_output('git', '-C', repo_path, 'fetch') - tag_cmd = ('git', '-C', repo_path, 'describe', 'origin/master', '--tags') + cmd_output('git', 'fetch', cwd=repo_path) + tag_cmd = ('git', 'describe', 'origin/master', '--tags') if tags_only: tag_cmd += ('--abbrev=0',) else: tag_cmd += ('--exact',) try: - rev = cmd_output(*tag_cmd)[1].strip() + rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip() except CalledProcessError: - tag_cmd = ('git', '-C', repo_path, 'rev-parse', 'origin/master') - rev = cmd_output(*tag_cmd)[1].strip() + tag_cmd = ('git', 'rev-parse', 'origin/master') + rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip() # Don't bother trying to update if our rev is the same if rev == repo_config['rev']: diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index 2e7658da5..e85a8f4a6 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -41,7 +41,7 @@ def make_archive(name, repo, ref, destdir): with tmpdir() as tempdir: # Clone the repository to the temporary directory cmd_output('git', 'clone', repo, tempdir) - cmd_output('git', '-C', tempdir, 'checkout', ref) + cmd_output('git', 'checkout', ref, cwd=tempdir) # We don't want the '.git' directory # It adds a bunch of size to the archive and we don't use it at diff --git a/pre_commit/store.py b/pre_commit/store.py index 735d67cf6..f5a9c2509 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -144,7 +144,7 @@ def clone_strategy(directory): env = no_git_env() def _git_cmd(*args): - return cmd_output('git', '-C', directory, *args, env=env) + return cmd_output('git', *args, cwd=directory, env=env) _git_cmd('clone', '--no-checkout', repo, '.') _git_cmd('reset', ref, '--hard') @@ -163,7 +163,7 @@ def make_local_strategy(directory): # initialize the git repository so it looks more like cloned repos def _git_cmd(*args): - cmd_output('git', '-C', directory, *args, env=env) + cmd_output('git', *args, cwd=directory, env=env) _git_cmd('init', '.') _git_cmd('config', 'remote.origin.url', '<>') diff --git a/testing/fixtures.py b/testing/fixtures.py index 15c06df60..fd5c7b43c 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -29,8 +29,8 @@ def git_dir(tempdir_factory): def make_repo(tempdir_factory, repo_source): path = git_dir(tempdir_factory) copy_tree_to_path(get_resource_path(repo_source), path) - cmd_output('git', '-C', path, 'add', '.') - cmd_output('git', '-C', path, 'commit', '-m', 'Add hooks') + cmd_output('git', 'add', '.', cwd=path) + cmd_output('git', 'commit', '-m', 'Add hooks', cwd=path) return path @@ -114,15 +114,14 @@ def write_config(directory, config, config_file=C.CONFIG_FILE): def add_config_to_repo(git_path, config, config_file=C.CONFIG_FILE): write_config(git_path, config, config_file=config_file) - cmd_output('git', '-C', git_path, 'add', config_file) - cmd_output('git', '-C', git_path, 'commit', '-m', 'Add hooks config') + cmd_output('git', 'add', config_file, cwd=git_path) + cmd_output('git', 'commit', '-m', 'Add hooks config', cwd=git_path) return git_path def remove_config_from_repo(git_path, config_file=C.CONFIG_FILE): - os.unlink(os.path.join(git_path, config_file)) - cmd_output('git', '-C', git_path, 'add', config_file) - cmd_output('git', '-C', git_path, 'commit', '-m', 'Remove hooks config') + cmd_output('git', 'rm', config_file, cwd=git_path) + cmd_output('git', 'commit', '-m', 'Remove hooks config', cwd=git_path) return git_path diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 0c6ffbaca..3e268c342 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -62,12 +62,12 @@ def test_autoupdate_old_revision_broken( path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path, check=False) - cmd_output('git', '-C', path, 'mv', C.MANIFEST_FILE, 'nope.yaml') - cmd_output('git', '-C', path, 'commit', '-m', 'simulate old repo') + cmd_output('git', 'mv', C.MANIFEST_FILE, 'nope.yaml', cwd=path) + cmd_output('git', 'commit', '-m', 'simulate old repo', cwd=path) # Assume this is the revision the user's old repository was at rev = git.head_rev(path) - cmd_output('git', '-C', path, 'mv', 'nope.yaml', C.MANIFEST_FILE) - cmd_output('git', '-C', path, 'commit', '-m', 'move hooks file') + cmd_output('git', 'mv', 'nope.yaml', C.MANIFEST_FILE, cwd=path) + cmd_output('git', 'commit', '-m', 'move hooks file', cwd=path) update_rev = git.head_rev(path) config['rev'] = rev @@ -86,7 +86,7 @@ def out_of_date_repo(tempdir_factory): original_rev = git.head_rev(path) # Make a commit - cmd_output('git', '-C', path, 'commit', '--allow-empty', '-m', 'foo') + cmd_output('git', 'commit', '--allow-empty', '-m', 'foo', cwd=path) head_rev = git.head_rev(path) yield auto_namedtuple( @@ -221,7 +221,7 @@ def test_loses_formatting_when_not_detectable( @pytest.fixture def tagged_repo(out_of_date_repo): - cmd_output('git', '-C', out_of_date_repo.path, 'tag', 'v1.2.3') + cmd_output('git', 'tag', 'v1.2.3', cwd=out_of_date_repo.path) yield out_of_date_repo @@ -240,8 +240,7 @@ def test_autoupdate_tagged_repo( @pytest.fixture def tagged_repo_with_more_commits(tagged_repo): - cmd = ('git', '-C', tagged_repo.path, 'commit', '--allow-empty', '-mfoo') - cmd_output(*cmd) + cmd_output('git', 'commit', '--allow-empty', '-mfoo', cwd=tagged_repo.path) yield tagged_repo @@ -268,8 +267,8 @@ def hook_disappearing_repo(tempdir_factory): get_resource_path('manifest_without_foo.yaml'), os.path.join(path, C.MANIFEST_FILE), ) - cmd_output('git', '-C', path, 'add', '.') - cmd_output('git', '-C', path, 'commit', '-m', 'Remove foo') + cmd_output('git', 'add', '.', cwd=path) + cmd_output('git', 'commit', '-m', 'Remove foo', cwd=path) yield auto_namedtuple(path=path, original_rev=original_rev) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index f83708ea8..491495f36 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -161,8 +161,8 @@ def test_install_pre_commit_and_run_custom_path(tempdir_factory): def test_install_in_submodule_and_run(tempdir_factory): src_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') parent_path = git_dir(tempdir_factory) - cmd_output('git', '-C', parent_path, 'submodule', 'add', src_path, 'sub') - cmd_output('git', '-C', parent_path, 'commit', '-m', 'foo') + cmd_output('git', 'submodule', 'add', src_path, 'sub', cwd=parent_path) + cmd_output('git', 'commit', '-m', 'foo', cwd=parent_path) sub_pth = os.path.join(parent_path, 'sub') with cwd(sub_pth): diff --git a/tests/conftest.py b/tests/conftest.py index 2d27a4a42..c0e13186e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -69,8 +69,8 @@ def _make_conflict(): def in_merge_conflict(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') open(os.path.join(path, 'dummy'), 'a').close() - cmd_output('git', '-C', path, 'add', 'dummy') - cmd_output('git', '-C', path, 'commit', '-m', 'Add config.') + cmd_output('git', 'add', 'dummy', cwd=path) + cmd_output('git', 'commit', '-m', 'Add config.', cwd=path) conflict_path = tempdir_factory.get() cmd_output('git', 'clone', path, conflict_path) @@ -83,8 +83,8 @@ def in_merge_conflict(tempdir_factory): def in_conflicting_submodule(tempdir_factory): git_dir_1 = git_dir(tempdir_factory) git_dir_2 = git_dir(tempdir_factory) - cmd_output('git', '-C', git_dir_2, 'commit', '--allow-empty', '-minit!') - cmd_output('git', '-C', git_dir_1, 'submodule', 'add', git_dir_2, 'sub') + cmd_output('git', 'commit', '--allow-empty', '-minit!', cwd=git_dir_2) + cmd_output('git', 'submodule', 'add', git_dir_2, 'sub', cwd=git_dir_1) with cwd(os.path.join(git_dir_1, 'sub')): _make_conflict() yield diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py index 65715acd0..60ecb7ac8 100644 --- a/tests/make_archives_test.py +++ b/tests/make_archives_test.py @@ -17,14 +17,14 @@ def test_make_archive(tempdir_factory): git_path = git_dir(tempdir_factory) # Add a files to the git directory open(os.path.join(git_path, 'foo'), 'a').close() - cmd_output('git', '-C', git_path, 'add', '.') - cmd_output('git', '-C', git_path, 'commit', '-m', 'foo') + cmd_output('git', 'add', '.', cwd=git_path) + cmd_output('git', 'commit', '-m', 'foo', cwd=git_path) # We'll use this rev head_rev = git.head_rev(git_path) # And check that this file doesn't exist open(os.path.join(git_path, 'bar'), 'a').close() - cmd_output('git', '-C', git_path, 'add', '.') - cmd_output('git', '-C', git_path, 'commit', '-m', 'bar') + cmd_output('git', 'add', '.', cwd=git_path) + cmd_output('git', 'commit', '-m', 'bar', cwd=git_path) # Do the thing archive_path = make_archives.make_archive( diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 932ee4b6f..b2af9fedb 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -199,7 +199,7 @@ def submodule_with_commits(tempdir_factory): def checkout_submodule(rev): - cmd_output('git', '-C', 'sub', 'checkout', rev) + cmd_output('git', 'checkout', rev, cwd='sub') @pytest.fixture From fbebd8449423be6703a2eaf84a950c16a1bc93f9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 12 Mar 2018 15:34:25 -0700 Subject: [PATCH 177/544] v1.8.1 --- CHANGELOG.md | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c242b2cb..3f6b9c09e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +1.8.1 +===== + +### Fixes +- Fix integration with go 1.10 and `pkg` directory + - #725 PR by @asottile +- Restore support for `git<1.8.5` (inadvertantly removed in 1.7.0) + - #723 issue by @JohnLyman. + - #724 PR by @asottile. + + 1.8.0 ===== diff --git a/setup.py b/setup.py index 4e17559fd..3fe083ec0 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.8.0', + version='1.8.1', author='Anthony Sottile', author_email='asottile@umich.edu', From af93bec4fda50c8d39aafe012fbaa68f2e667ca6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 17 Mar 2018 20:02:06 -0700 Subject: [PATCH 178/544] Fix regression: try-repo from relative path --- pre_commit/store.py | 4 +++- tests/commands/try_repo_test.py | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index f5a9c2509..8251e21b8 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -143,10 +143,12 @@ def clone(self, repo, ref, deps=()): def clone_strategy(directory): env = no_git_env() + cmd = ('git', 'clone', '--no-checkout', repo, directory) + cmd_output(*cmd, env=env) + def _git_cmd(*args): return cmd_output('git', *args, cwd=directory, env=env) - _git_cmd('clone', '--no-checkout', repo, '.') _git_cmd('reset', ref, '--hard') _git_cmd('submodule', 'update', '--init', '--recursive') diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index 4fb0755c2..490cdd563 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -1,6 +1,7 @@ from __future__ import absolute_import from __future__ import unicode_literals +import os.path import re from pre_commit.commands.try_repo import try_repo @@ -69,3 +70,13 @@ def test_try_repo_with_specific_hook(cap_out, tempdir_factory): config, ) assert rest == '[bash_hook] Bash hook................................(no files to check)Skipped\n' # noqa + + +def test_try_repo_relative_path(cap_out, tempdir_factory): + repo = make_repo(tempdir_factory, 'modified_file_returns_zero_repo') + with cwd(git_dir(tempdir_factory)): + open('test-file', 'a').close() + cmd_output('git', 'add', '.') + relative_repo = os.path.relpath(repo, '.') + # previously crashed on cloning a relative path + assert not try_repo(try_repo_opts(relative_repo, hook='bash_hook')) From 834ed0f229a39c986b241374f6d338632e003b5f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 17 Mar 2018 20:40:02 -0700 Subject: [PATCH 179/544] v1.8.2 --- CHANGELOG.md | 9 +++++++++ setup.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f6b9c09e..b3fe15cda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +1.8.2 +===== + +### Fixes +- Fix cloning relative paths (regression in 1.7.0) + - #728 issue by @jdswensen. + - #729 PR by @asottile. + + 1.8.1 ===== diff --git a/setup.py b/setup.py index 3fe083ec0..48aabd2cc 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.8.1', + version='1.8.2', author='Anthony Sottile', author_email='asottile@umich.edu', From d6825fa0fca4a309f99d5a733eecbe9d5d8abdbd Mon Sep 17 00:00:00 2001 From: Jonas Obrist Date: Fri, 27 Apr 2018 17:13:47 +0900 Subject: [PATCH 180/544] added python venv language --- pre_commit/languages/all.py | 2 + pre_commit/languages/python_venv.py | 73 +++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 pre_commit/languages/python_venv.py diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index a56f7e79e..504c28a04 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -7,6 +7,7 @@ from pre_commit.languages import pcre from pre_commit.languages import pygrep from pre_commit.languages import python +from pre_commit.languages import python_venv from pre_commit.languages import ruby from pre_commit.languages import script from pre_commit.languages import swift @@ -57,6 +58,7 @@ 'pcre': pcre, 'pygrep': pygrep, 'python': python, + 'python_venv': python_venv, 'ruby': ruby, 'script': script, 'swift': swift, diff --git a/pre_commit/languages/python_venv.py b/pre_commit/languages/python_venv.py new file mode 100644 index 000000000..26389c657 --- /dev/null +++ b/pre_commit/languages/python_venv.py @@ -0,0 +1,73 @@ +from __future__ import unicode_literals + +import contextlib +import os +import sys + +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import UNSET +from pre_commit.envcontext import Var +from pre_commit.languages import helpers +from pre_commit.languages.python import get_default_version # noqa: F401 +from pre_commit.languages.python import norm_version +from pre_commit.util import clean_path_on_failure +from pre_commit.util import cmd_output +from pre_commit.xargs import xargs + + +ENVIRONMENT_DIR = 'py_venv' + + +def bin_dir(venv): + """On windows there's a different directory for the virtualenv""" + bin_part = 'Scripts' if os.name == 'nt' else 'bin' + return os.path.join(venv, bin_part) + + +def get_env_patch(venv): + return ( + ('PYTHONHOME', UNSET), + ('VIRTUAL_ENV', venv), + ('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))), + ) + + +@contextlib.contextmanager +def in_env(prefix, language_version): + envdir = prefix.path( + helpers.environment_dir(ENVIRONMENT_DIR, language_version), + ) + with envcontext(get_env_patch(envdir)): + yield + + +def healthy(prefix, language_version): + with in_env(prefix, language_version): + retcode, _, _ = cmd_output( + 'python', '-c', 'import ctypes, datetime, io, os, ssl, weakref', + retcode=None, + ) + return retcode == 0 + + +def install_environment(prefix, version, additional_dependencies): + additional_dependencies = tuple(additional_dependencies) + directory = helpers.environment_dir(ENVIRONMENT_DIR, version) + + # Install a virtualenv + env_dir = prefix.path(directory) + with clean_path_on_failure(env_dir): + if version != 'default': + executable = norm_version(version) + else: + executable = os.path.realpath(sys.executable) + cmd_output(executable, '-m', 'venv', env_dir, cwd='/') + with in_env(prefix, version): + helpers.run_setup_cmd( + prefix, ('pip', 'install', '.') + additional_dependencies, + ) + + +def run_hook(prefix, hook, file_args): + with in_env(prefix, hook['language_version']): + return xargs(helpers.to_cmd(hook), file_args) From e4471e4bc866e23e731b5b45c7b201d7827799cc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Apr 2018 09:33:20 -0400 Subject: [PATCH 181/544] Replace legacy wheel metadata Committed via https://github.com/asottile/all-repos --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index e57d130e3..2be683657 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,2 @@ -[wheel] +[bdist_wheel] universal = True From e55f51fb14299954c63f033b1178f051009efd93 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 2 May 2018 11:25:16 -0400 Subject: [PATCH 182/544] Remove unused __popen DI --- pre_commit/util.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index 882ebb006..bcb47c3fc 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -144,7 +144,6 @@ def to_text(self): def cmd_output(*cmd, **kwargs): retcode = kwargs.pop('retcode', 0) encoding = kwargs.pop('encoding', 'UTF-8') - __popen = kwargs.pop('__popen', subprocess.Popen) popen_kwargs = { 'stdin': subprocess.PIPE, @@ -165,7 +164,7 @@ def cmd_output(*cmd, **kwargs): returncode, stdout, stderr = e.to_output() else: popen_kwargs.update(kwargs) - proc = __popen(cmd, **popen_kwargs) + proc = subprocess.Popen(cmd, **popen_kwargs) stdout, stderr = proc.communicate() returncode = proc.returncode if encoding is not None and stdout is not None: From 49ff78e3ea3a9b358dab3d74e63217af5deff27c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 3 May 2018 11:26:40 -0300 Subject: [PATCH 183/544] Add MANIFEST file to include license in sdist --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000..1aba38f67 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include LICENSE From e8954e2bf3c02bef1eae5ea4d3ff36e026b74c90 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 12 May 2018 09:26:20 -0700 Subject: [PATCH 184/544] Simplify python_venv interface --- pre_commit/languages/python.py | 84 +++++++++++++++-------------- pre_commit/languages/python_venv.py | 69 +++--------------------- 2 files changed, 51 insertions(+), 102 deletions(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 7fc5443ef..0840b9005 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -16,6 +16,7 @@ ENVIRONMENT_DIR = 'py_env' +HEALTH_MODS = ('datetime', 'io', 'os', 'ssl', 'weakref') def bin_dir(venv): @@ -32,15 +33,6 @@ def get_env_patch(venv): ) -@contextlib.contextmanager -def in_env(prefix, language_version): - envdir = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, language_version), - ) - with envcontext(get_env_patch(envdir)): - yield - - def _find_by_py_launcher(version): # pragma: no cover (windows only) if version.startswith('python'): try: @@ -98,15 +90,6 @@ def get_default_version(): return get_default_version() -def healthy(prefix, language_version): - with in_env(prefix, language_version): - retcode, _, _ = cmd_output( - 'python', '-c', 'import ctypes, datetime, io, os, ssl, weakref', - retcode=None, - ) - return retcode == 0 - - def norm_version(version): if os.name == 'nt': # pragma: no cover (windows) # Try looking up by name @@ -123,30 +106,53 @@ def norm_version(version): if version.startswith('python'): return r'C:\{}\python.exe'.format(version.replace('.', '')) - # Otherwise assume it is a path + # Otherwise assume it is a path return os.path.expanduser(version) -def install_environment(prefix, version, additional_dependencies): - additional_dependencies = tuple(additional_dependencies) - directory = helpers.environment_dir(ENVIRONMENT_DIR, version) - - # Install a virtualenv - env_dir = prefix.path(directory) - with clean_path_on_failure(env_dir): - venv_cmd = [sys.executable, '-m', 'virtualenv', env_dir] - if version != 'default': - venv_cmd.extend(['-p', norm_version(version)]) - else: - venv_cmd.extend(['-p', os.path.realpath(sys.executable)]) - venv_env = dict(os.environ, VIRTUALENV_NO_DOWNLOAD='1') - cmd_output(*venv_cmd, cwd='/', env=venv_env) - with in_env(prefix, version): - helpers.run_setup_cmd( - prefix, ('pip', 'install', '.') + additional_dependencies, +def py_interface(_dir, _make_venv): + @contextlib.contextmanager + def in_env(prefix, language_version): + envdir = prefix.path(helpers.environment_dir(_dir, language_version)) + with envcontext(get_env_patch(envdir)): + yield + + def healthy(prefix, language_version): + with in_env(prefix, language_version): + retcode, _, _ = cmd_output( + 'python', '-c', 'import {}'.format(','.join(HEALTH_MODS)), + retcode=None, ) + return retcode == 0 + + def run_hook(prefix, hook, file_args): + with in_env(prefix, hook['language_version']): + return xargs(helpers.to_cmd(hook), file_args) + + def install_environment(prefix, version, additional_dependencies): + additional_dependencies = tuple(additional_dependencies) + directory = helpers.environment_dir(_dir, version) + + env_dir = prefix.path(directory) + with clean_path_on_failure(env_dir): + if version != 'default': + python = norm_version(version) + else: + python = os.path.realpath(sys.executable) + _make_venv(env_dir, python) + with in_env(prefix, version): + helpers.run_setup_cmd( + prefix, ('pip', 'install', '.') + additional_dependencies, + ) + + return in_env, healthy, run_hook, install_environment + + +def make_venv(envdir, python): + env = dict(os.environ, VIRTUALENV_NO_DOWNLOAD='1') + cmd = (sys.executable, '-mvirtualenv', envdir, '-p', python) + cmd_output(*cmd, env=env, cwd='/') -def run_hook(prefix, hook, file_args): - with in_env(prefix, hook['language_version']): - return xargs(helpers.to_cmd(hook), file_args) +_interface = py_interface(ENVIRONMENT_DIR, make_venv) +in_env, healthy, run_hook, install_environment = _interface diff --git a/pre_commit/languages/python_venv.py b/pre_commit/languages/python_venv.py index 26389c657..20613a49b 100644 --- a/pre_commit/languages/python_venv.py +++ b/pre_commit/languages/python_venv.py @@ -1,73 +1,16 @@ from __future__ import unicode_literals -import contextlib -import os -import sys - -from pre_commit.envcontext import envcontext -from pre_commit.envcontext import UNSET -from pre_commit.envcontext import Var -from pre_commit.languages import helpers -from pre_commit.languages.python import get_default_version # noqa: F401 -from pre_commit.languages.python import norm_version -from pre_commit.util import clean_path_on_failure +from pre_commit.languages import python from pre_commit.util import cmd_output -from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'py_venv' -def bin_dir(venv): - """On windows there's a different directory for the virtualenv""" - bin_part = 'Scripts' if os.name == 'nt' else 'bin' - return os.path.join(venv, bin_part) - - -def get_env_patch(venv): - return ( - ('PYTHONHOME', UNSET), - ('VIRTUAL_ENV', venv), - ('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))), - ) - - -@contextlib.contextmanager -def in_env(prefix, language_version): - envdir = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, language_version), - ) - with envcontext(get_env_patch(envdir)): - yield - - -def healthy(prefix, language_version): - with in_env(prefix, language_version): - retcode, _, _ = cmd_output( - 'python', '-c', 'import ctypes, datetime, io, os, ssl, weakref', - retcode=None, - ) - return retcode == 0 - - -def install_environment(prefix, version, additional_dependencies): - additional_dependencies = tuple(additional_dependencies) - directory = helpers.environment_dir(ENVIRONMENT_DIR, version) - - # Install a virtualenv - env_dir = prefix.path(directory) - with clean_path_on_failure(env_dir): - if version != 'default': - executable = norm_version(version) - else: - executable = os.path.realpath(sys.executable) - cmd_output(executable, '-m', 'venv', env_dir, cwd='/') - with in_env(prefix, version): - helpers.run_setup_cmd( - prefix, ('pip', 'install', '.') + additional_dependencies, - ) +def make_venv(envdir, python): + cmd_output(python, '-mvenv', envdir, cwd='/') -def run_hook(prefix, hook, file_args): - with in_env(prefix, hook['language_version']): - return xargs(helpers.to_cmd(hook), file_args) +get_default_version = python.get_default_version +_interface = python.py_interface(ENVIRONMENT_DIR, make_venv) +in_env, healthy, run_hook, install_environment = _interface From cd8179a974daff8c3b664086a89d7daa574207f4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 12 May 2018 10:01:14 -0700 Subject: [PATCH 185/544] Apply relative files to try-repo also --- pre_commit/main.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 18db533a8..9b7f14168 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -212,22 +212,21 @@ def main(argv=None): ) help.add_argument('help_cmd', nargs='?', help='Command to show help for.') - # Argparse doesn't really provide a way to use a `default` subparser + # argparse doesn't really provide a way to use a `default` subparser if len(argv) == 0: argv = ['run'] args = parser.parse_args(argv) - if args.command == 'run': + + if args.command == 'help' and args.help_cmd: + parser.parse_args([args.help_cmd, '--help']) + elif args.command == 'help': + parser.parse_args(['--help']) + elif args.command in {'run', 'try-repo'}: args.files = [ os.path.relpath(os.path.abspath(filename), git.get_root()) for filename in args.files ] - if args.command == 'help': - if args.help_cmd: - parser.parse_args([args.help_cmd, '--help']) - else: - parser.parse_args(['--help']) - with error_handler(): add_logging_handler(args.color) runner = Runner.create(args.config) From 3d49db7851dedaa077bd23d5f869e24ea7732276 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 12 May 2018 11:17:41 -0700 Subject: [PATCH 186/544] Set `skip_covered = True` in .coveragerc --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 958d944aa..2dca7634c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -11,6 +11,7 @@ omit = [report] show_missing = True +skip_covered = True exclude_lines = # Have to re-enable the standard pragma \#\s*pragma: no cover From b5af5a5b2761a09e8164e38dd520cd59cdd23ed0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 12 May 2018 10:21:46 -0700 Subject: [PATCH 187/544] Add test for python_venv language --- pre_commit/languages/python.py | 4 ++-- .../{python3_hook/main.py => py3_hook.py} | 2 +- testing/resources/python3_hooks_repo/setup.py | 7 ++----- .../python_hooks_repo/{foo/main.py => foo.py} | 2 +- .../resources/python_hooks_repo/foo/__init__.py | 0 testing/resources/python_hooks_repo/setup.py | 9 +++------ .../python_venv_hooks_repo/.pre-commit-hooks.yaml | 5 +++++ testing/resources/python_venv_hooks_repo/foo.py | 9 +++++++++ .../foo}/__init__.py | 0 testing/resources/python_venv_hooks_repo/setup.py | 8 ++++++++ testing/util.py | 14 ++++++++++++++ tests/repository_test.py | 10 ++++++++++ 12 files changed, 55 insertions(+), 15 deletions(-) rename testing/resources/python3_hooks_repo/{python3_hook/main.py => py3_hook.py} (92%) rename testing/resources/python_hooks_repo/{foo/main.py => foo.py} (90%) delete mode 100644 testing/resources/python_hooks_repo/foo/__init__.py create mode 100644 testing/resources/python_venv_hooks_repo/.pre-commit-hooks.yaml create mode 100644 testing/resources/python_venv_hooks_repo/foo.py rename testing/resources/{python3_hooks_repo/python3_hook => python_venv_hooks_repo/foo}/__init__.py (100%) create mode 100644 testing/resources/python_venv_hooks_repo/setup.py diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 0840b9005..ee7b2a4f1 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -16,7 +16,6 @@ ENVIRONMENT_DIR = 'py_env' -HEALTH_MODS = ('datetime', 'io', 'os', 'ssl', 'weakref') def bin_dir(venv): @@ -120,7 +119,8 @@ def in_env(prefix, language_version): def healthy(prefix, language_version): with in_env(prefix, language_version): retcode, _, _ = cmd_output( - 'python', '-c', 'import {}'.format(','.join(HEALTH_MODS)), + 'python', '-c', + 'import ctypes, datetime, io, os, ssl, weakref', retcode=None, ) return retcode == 0 diff --git a/testing/resources/python3_hooks_repo/python3_hook/main.py b/testing/resources/python3_hooks_repo/py3_hook.py similarity index 92% rename from testing/resources/python3_hooks_repo/python3_hook/main.py rename to testing/resources/python3_hooks_repo/py3_hook.py index 04f974e64..f0f880886 100644 --- a/testing/resources/python3_hooks_repo/python3_hook/main.py +++ b/testing/resources/python3_hooks_repo/py3_hook.py @@ -3,7 +3,7 @@ import sys -def func(): +def main(): print(sys.version_info[0]) print(repr(sys.argv[1:])) print('Hello World') diff --git a/testing/resources/python3_hooks_repo/setup.py b/testing/resources/python3_hooks_repo/setup.py index bf7690c03..9125dc1df 100644 --- a/testing/resources/python3_hooks_repo/setup.py +++ b/testing/resources/python3_hooks_repo/setup.py @@ -1,11 +1,8 @@ -from setuptools import find_packages from setuptools import setup setup( name='python3_hook', version='0.0.0', - packages=find_packages('.'), - entry_points={ - 'console_scripts': ['python3-hook = python3_hook.main:func'], - }, + py_modules=['py3_hook'], + entry_points={'console_scripts': ['python3-hook = py3_hook:main']}, ) diff --git a/testing/resources/python_hooks_repo/foo/main.py b/testing/resources/python_hooks_repo/foo.py similarity index 90% rename from testing/resources/python_hooks_repo/foo/main.py rename to testing/resources/python_hooks_repo/foo.py index 78c2c0f74..412a5c625 100644 --- a/testing/resources/python_hooks_repo/foo/main.py +++ b/testing/resources/python_hooks_repo/foo.py @@ -3,7 +3,7 @@ import sys -def func(): +def main(): print(repr(sys.argv[1:])) print('Hello World') return 0 diff --git a/testing/resources/python_hooks_repo/foo/__init__.py b/testing/resources/python_hooks_repo/foo/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/testing/resources/python_hooks_repo/setup.py b/testing/resources/python_hooks_repo/setup.py index 556dd8f50..0559271ee 100644 --- a/testing/resources/python_hooks_repo/setup.py +++ b/testing/resources/python_hooks_repo/setup.py @@ -1,11 +1,8 @@ -from setuptools import find_packages from setuptools import setup setup( - name='Foo', + name='foo', version='0.0.0', - packages=find_packages('.'), - entry_points={ - 'console_scripts': ['foo = foo.main:func'], - }, + py_modules=['foo'], + entry_points={'console_scripts': ['foo = foo:main']}, ) diff --git a/testing/resources/python_venv_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/python_venv_hooks_repo/.pre-commit-hooks.yaml new file mode 100644 index 000000000..a666ed87a --- /dev/null +++ b/testing/resources/python_venv_hooks_repo/.pre-commit-hooks.yaml @@ -0,0 +1,5 @@ +- id: foo + name: Foo + entry: foo + language: python_venv + files: \.py$ diff --git a/testing/resources/python_venv_hooks_repo/foo.py b/testing/resources/python_venv_hooks_repo/foo.py new file mode 100644 index 000000000..412a5c625 --- /dev/null +++ b/testing/resources/python_venv_hooks_repo/foo.py @@ -0,0 +1,9 @@ +from __future__ import print_function + +import sys + + +def main(): + print(repr(sys.argv[1:])) + print('Hello World') + return 0 diff --git a/testing/resources/python3_hooks_repo/python3_hook/__init__.py b/testing/resources/python_venv_hooks_repo/foo/__init__.py similarity index 100% rename from testing/resources/python3_hooks_repo/python3_hook/__init__.py rename to testing/resources/python_venv_hooks_repo/foo/__init__.py diff --git a/testing/resources/python_venv_hooks_repo/setup.py b/testing/resources/python_venv_hooks_repo/setup.py new file mode 100644 index 000000000..0559271ee --- /dev/null +++ b/testing/resources/python_venv_hooks_repo/setup.py @@ -0,0 +1,8 @@ +from setuptools import setup + +setup( + name='foo', + version='0.0.0', + py_modules=['foo'], + entry_points={'console_scripts': ['foo = foo:main']}, +) diff --git a/testing/util.py b/testing/util.py index 025bc0bba..ae5ae3384 100644 --- a/testing/util.py +++ b/testing/util.py @@ -78,6 +78,20 @@ def platform_supports_pcre(): ) +def supports_venv(): # pragma: no cover (platform specific) + try: + __import__('ensurepip') + __import__('venv') + return True + except ImportError: + return False + + +xfailif_no_venv = pytest.mark.xfail( + not supports_venv(), reason='Does not support venv module', +) + + def run_opts( all_files=False, files=(), diff --git a/tests/repository_test.py b/tests/repository_test.py index 63b9f1c9c..67b8f3f6c 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -33,6 +33,7 @@ from testing.util import skipif_cant_run_swift from testing.util import xfailif_broken_deep_listdir from testing.util import xfailif_no_pcre_support +from testing.util import xfailif_no_venv from testing.util import xfailif_windows_no_ruby @@ -111,6 +112,15 @@ def test_python_hook_weird_setup_cfg(tempdir_factory, store): ) +@xfailif_no_venv +def test_python_venv(tempdir_factory, store): # pragma: no cover (no venv) + _test_hook_repo( + tempdir_factory, store, 'python_venv_hooks_repo', + 'foo', [os.devnull], + b"['" + five.to_bytes(os.devnull) + b"']\nHello World\n", + ) + + @pytest.mark.integration def test_switch_language_versions_doesnt_clobber(tempdir_factory, store): # We're using the python3 repo because it prints the python version From 3555a2b1584706f4a41c01d4fa554e5fa91cba28 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 21 May 2018 20:48:08 -0700 Subject: [PATCH 188/544] v1.9.0 --- CHANGELOG.md | 17 +++++++++++++++++ setup.py | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3fe15cda..6beb98420 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +1.9.0 +===== + +### Features +- Add new `python_venv` language which uses the `venv` module instead of + `virtualenv` + - #631 issue by @dongyuzheng. + - #739 PR by @ojii. +- Include `LICENSE` in distribution + - #745 issue by @nicoddemus. + - #746 PR by @nicoddemus. + +### Fixes +- Normalize relative paths for `pre-commit try-repo` + - #750 PR by @asottile. + + 1.8.2 ===== diff --git a/setup.py b/setup.py index 48aabd2cc..c45047744 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.8.2', + version='1.9.0', author='Anthony Sottile', author_email='asottile@umich.edu', From f88e007f52ca985fbaa63320a21557e20ee12eeb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 21 May 2018 21:38:36 -0700 Subject: [PATCH 189/544] Fix test since pip 10 changed output --- .../.pre-commit-hooks.yaml | 6 ----- .../resources/not_installable_repo/setup.py | 17 ------------- tests/commands/run_test.py | 25 ------------------- tests/languages/helpers_test.py | 18 +++++++++++++ 4 files changed, 18 insertions(+), 48 deletions(-) delete mode 100644 testing/resources/not_installable_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/not_installable_repo/setup.py diff --git a/testing/resources/not_installable_repo/.pre-commit-hooks.yaml b/testing/resources/not_installable_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 48c1f9efe..000000000 --- a/testing/resources/not_installable_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,6 +0,0 @@ -- id: foo - name: Foo - entry: foo - language: python - language_version: python2.7 - files: \.py$ diff --git a/testing/resources/not_installable_repo/setup.py b/testing/resources/not_installable_repo/setup.py deleted file mode 100644 index ae5f6338c..000000000 --- a/testing/resources/not_installable_repo/setup.py +++ /dev/null @@ -1,17 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals - -import sys - - -def main(): - # Intentionally write mixed encoding to the output. This should not crash - # pre-commit and should write bytes to the output. - sys.stderr.write('☃'.encode('UTF-8') + '²'.encode('latin1') + b'\n') - # Return 1 to indicate failures - return 1 - - -if __name__ == '__main__': - exit(main()) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index d664e8013..cd32c5f65 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -479,31 +479,6 @@ def test_stdout_write_bug_py26( assert 'UnicodeDecodeError' not in stdout -def test_hook_install_failure(mock_out_store_directory, tempdir_factory): - git_path = make_consuming_repo(tempdir_factory, 'not_installable_repo') - with cwd(git_path): - install(Runner(git_path, C.CONFIG_FILE)) - - _, stdout, _ = cmd_output_mocked_pre_commit_home( - 'git', 'commit', '-m', 'Commit!', - # git commit puts pre-commit to stderr - stderr=subprocess.STDOUT, - retcode=None, - encoding=None, - tempdir_factory=tempdir_factory, - ) - assert b'UnicodeDecodeError' not in stdout - # Doesn't actually happen, but a reasonable assertion - assert b'UnicodeEncodeError' not in stdout - - # Sanity check our output - assert ( - b'An unexpected error has occurred: CalledProcessError: ' in - stdout - ) - assert '☃'.encode('UTF-8') + '²'.encode('latin1') in stdout - - def test_lots_of_files(mock_out_store_directory, tempdir_factory): # windows xargs seems to have a bug, here's a regression test for # our workaround diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index 7019e260f..ada2095b6 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -1,7 +1,13 @@ from __future__ import absolute_import from __future__ import unicode_literals +import sys + +import pytest + from pre_commit.languages import helpers +from pre_commit.prefix import Prefix +from pre_commit.util import CalledProcessError def test_basic_get_default_version(): @@ -10,3 +16,15 @@ def test_basic_get_default_version(): def test_basic_healthy(): assert helpers.basic_healthy(None, None) is True + + +def test_failed_setup_command_does_not_unicode_error(): + script = ( + 'import sys\n' + "getattr(sys.stderr, 'buffer', sys.stderr).write(b'\\x81\\xfe')\n" + 'exit(1)\n' + ) + + # an assertion that this does not raise `UnicodeError` + with pytest.raises(CalledProcessError): + helpers.run_setup_cmd(Prefix('.'), (sys.executable, '-c', script)) From 7f29fd55915992ed4570461d0af40f5750bc8d91 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 21 May 2018 22:20:37 -0700 Subject: [PATCH 190/544] Adjust feature detection for 2.7.15 --- testing/util.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/testing/util.py b/testing/util.py index ae5ae3384..42cf07eb4 100644 --- a/testing/util.py +++ b/testing/util.py @@ -52,7 +52,11 @@ def broken_deep_listdir(): # pragma: no cover (platform specific) os.listdir(str('\\\\?\\') + os.path.abspath(str('.'))) except OSError: return True - else: + try: + os.listdir(b'\\\\?\C:' + b'\\' * 300) + except TypeError: + return True + except OSError: return False From 7f85da1b9dedf8224574fb1264c2b182c36290be Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Tue, 15 May 2018 20:59:18 -0700 Subject: [PATCH 191/544] Add Rust support --- pre_commit/languages/all.py | 2 ++ pre_commit/languages/rust.py | 68 ++++++++++++++++++++++++++++++++++++ setup.py | 1 + 3 files changed, 71 insertions(+) create mode 100644 pre_commit/languages/rust.py diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 504c28a04..be74ffd3a 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -9,6 +9,7 @@ from pre_commit.languages import python from pre_commit.languages import python_venv from pre_commit.languages import ruby +from pre_commit.languages import rust from pre_commit.languages import script from pre_commit.languages import swift from pre_commit.languages import system @@ -60,6 +61,7 @@ 'python': python, 'python_venv': python_venv, 'ruby': ruby, + 'rust': rust, 'script': script, 'swift': swift, 'system': system, diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py new file mode 100644 index 000000000..e6884c34c --- /dev/null +++ b/pre_commit/languages/rust.py @@ -0,0 +1,68 @@ +from __future__ import unicode_literals + +import contextlib +import os.path + +import toml + +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import Var +from pre_commit.languages import helpers +from pre_commit.util import clean_path_on_failure +from pre_commit.util import cmd_output +from pre_commit.xargs import xargs + + +ENVIRONMENT_DIR = 'rustenv' +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy + + +def get_env_patch(target_dir): + return ( + ( + 'PATH', + (os.path.join(target_dir, 'release'), os.pathsep, Var('PATH')), + ), + ) + + +@contextlib.contextmanager +def in_env(prefix): + target_dir = prefix.path( + helpers.environment_dir(ENVIRONMENT_DIR, 'default'), + ) + with envcontext(get_env_patch(target_dir)): + yield + + +def _add_dependencies(cargo_toml_path, additional_dependencies): + with open(cargo_toml_path, 'r+') as f: + cargo_toml = toml.load(f) + for dep in additional_dependencies: + name, _, spec = dep.partition(':') + cargo_toml['dependencies'][name] = spec or '*' + f.seek(0) + toml.dump(cargo_toml, f) + f.truncate() + + +def install_environment(prefix, version, additional_dependencies): + helpers.assert_version_default('rust', version) + directory = prefix.path( + helpers.environment_dir(ENVIRONMENT_DIR, 'default'), + ) + + if len(additional_dependencies) > 0: + _add_dependencies(prefix.path('Cargo.toml'), additional_dependencies) + + with clean_path_on_failure(directory): + cmd_output( + 'cargo', 'build', '--release', '--bins', '--target-dir', directory, + cwd=prefix.prefix_dir, + ) + + +def run_hook(prefix, hook, file_args): + with in_env(prefix): + return xargs(helpers.to_cmd(hook), file_args) diff --git a/setup.py b/setup.py index c45047744..831dc0001 100644 --- a/setup.py +++ b/setup.py @@ -42,6 +42,7 @@ 'nodeenv>=0.11.1', 'pyyaml', 'six', + 'toml', 'virtualenv', ], entry_points={ From 2a37fcd3fe53b7d03e2e563a7915446a8d87f407 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Mon, 21 May 2018 22:02:03 -0700 Subject: [PATCH 192/544] Add support for Rust CLI dependencies Also consistently build the hook using `cargo install`. --- pre_commit/languages/rust.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index e6884c34c..541a333c9 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -22,7 +22,7 @@ def get_env_patch(target_dir): return ( ( 'PATH', - (os.path.join(target_dir, 'release'), os.pathsep, Var('PATH')), + (os.path.join(target_dir, 'bin'), os.pathsep, Var('PATH')), ), ) @@ -47,20 +47,36 @@ def _add_dependencies(cargo_toml_path, additional_dependencies): f.truncate() -def install_environment(prefix, version, additional_dependencies): +def install_environment(prefix, version, additional_deps): helpers.assert_version_default('rust', version) directory = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, 'default'), ) - if len(additional_dependencies) > 0: - _add_dependencies(prefix.path('Cargo.toml'), additional_dependencies) + # There are two cases where we might want to specify more dependencies: + # as dependencies for the library being built, and as binary packages + # to be `cargo install`'d. + # + # Unlike e.g. Python, if we just `cargo install` a library, it won't be + # used for compilation. And if we add a crate providing a binary to the + # `Cargo.toml`, the binary won't be built. + # + # Because of this, we allow specifying "cli" dependencies by prefixing + # with 'cli:'. + cli_deps = {dep for dep in additional_deps if dep.startswith('cli:')} + lib_deps = set(additional_deps) - cli_deps + + if len(lib_deps) > 0: + _add_dependencies(prefix.path('Cargo.toml'), lib_deps) with clean_path_on_failure(directory): - cmd_output( - 'cargo', 'build', '--release', '--bins', '--target-dir', directory, - cwd=prefix.prefix_dir, - ) + packages_to_install = {()} | {(dep[len('cli:'):],) for dep in cli_deps} + + for package in packages_to_install: + cmd_output( + 'cargo', 'install', '--bins', '--root', directory, *package, + cwd=prefix.prefix_dir + ) def run_hook(prefix, hook, file_args): From b4edf2ce50df13100eb600c7232670edc03a6651 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Mon, 21 May 2018 22:57:30 -0700 Subject: [PATCH 193/544] Add tests for Rust --- pre_commit/languages/rust.py | 18 ++++++-- .../rust_hooks_repo/.pre-commit-hooks.yaml | 5 ++ testing/resources/rust_hooks_repo/Cargo.lock | 3 ++ testing/resources/rust_hooks_repo/Cargo.toml | 3 ++ testing/resources/rust_hooks_repo/src/main.rs | 3 ++ tests/repository_test.py | 46 +++++++++++++++++++ 6 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 testing/resources/rust_hooks_repo/.pre-commit-hooks.yaml create mode 100644 testing/resources/rust_hooks_repo/Cargo.lock create mode 100644 testing/resources/rust_hooks_repo/Cargo.toml create mode 100644 testing/resources/rust_hooks_repo/src/main.rs diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 541a333c9..41053f889 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -39,6 +39,7 @@ def in_env(prefix): def _add_dependencies(cargo_toml_path, additional_dependencies): with open(cargo_toml_path, 'r+') as f: cargo_toml = toml.load(f) + cargo_toml.setdefault('dependencies', {}) for dep in additional_dependencies: name, _, spec = dep.partition(':') cargo_toml['dependencies'][name] = spec or '*' @@ -47,7 +48,7 @@ def _add_dependencies(cargo_toml_path, additional_dependencies): f.truncate() -def install_environment(prefix, version, additional_deps): +def install_environment(prefix, version, additional_dependencies): helpers.assert_version_default('rust', version) directory = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, 'default'), @@ -63,14 +64,23 @@ def install_environment(prefix, version, additional_deps): # # Because of this, we allow specifying "cli" dependencies by prefixing # with 'cli:'. - cli_deps = {dep for dep in additional_deps if dep.startswith('cli:')} - lib_deps = set(additional_deps) - cli_deps + cli_deps = { + dep for dep in additional_dependencies if dep.startswith('cli:') + } + lib_deps = set(additional_dependencies) - cli_deps if len(lib_deps) > 0: _add_dependencies(prefix.path('Cargo.toml'), lib_deps) with clean_path_on_failure(directory): - packages_to_install = {()} | {(dep[len('cli:'):],) for dep in cli_deps} + packages_to_install = {()} + for cli_dep in cli_deps: + cli_dep = cli_dep[len('cli:'):] + package, _, version = cli_dep.partition(':') + if version != '': + packages_to_install.add((package, '--version', version)) + else: + packages_to_install.add((package,)) for package in packages_to_install: cmd_output( diff --git a/testing/resources/rust_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/rust_hooks_repo/.pre-commit-hooks.yaml new file mode 100644 index 000000000..df1269ff8 --- /dev/null +++ b/testing/resources/rust_hooks_repo/.pre-commit-hooks.yaml @@ -0,0 +1,5 @@ +- id: rust-hook + name: rust example hook + entry: rust-hello-world + language: rust + files: '' diff --git a/testing/resources/rust_hooks_repo/Cargo.lock b/testing/resources/rust_hooks_repo/Cargo.lock new file mode 100644 index 000000000..36fbfda2b --- /dev/null +++ b/testing/resources/rust_hooks_repo/Cargo.lock @@ -0,0 +1,3 @@ +[[package]] +name = "rust-hello-world" +version = "0.1.0" diff --git a/testing/resources/rust_hooks_repo/Cargo.toml b/testing/resources/rust_hooks_repo/Cargo.toml new file mode 100644 index 000000000..cd83b4358 --- /dev/null +++ b/testing/resources/rust_hooks_repo/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "rust-hello-world" +version = "0.1.0" diff --git a/testing/resources/rust_hooks_repo/src/main.rs b/testing/resources/rust_hooks_repo/src/main.rs new file mode 100644 index 000000000..ad379d6ea --- /dev/null +++ b/testing/resources/rust_hooks_repo/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("hello world"); +} diff --git a/tests/repository_test.py b/tests/repository_test.py index 67b8f3f6c..6fece071f 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -20,6 +20,7 @@ from pre_commit.languages import pcre from pre_commit.languages import python from pre_commit.languages import ruby +from pre_commit.languages import rust from pre_commit.repository import Repository from pre_commit.util import cmd_output from testing.fixtures import config_with_local_hooks @@ -282,6 +283,51 @@ def test_golang_hook(tempdir_factory, store): ) +@pytest.mark.integration +def test_rust_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'rust_hooks_repo', + 'rust-hook', [], b'hello world\n', + ) + + +@pytest.mark.integration +@pytest.mark.parametrize('dep', ('cli:shellharden:3.1.0', 'cli:shellharden')) +def test_additional_rust_cli_dependencies_installed( + tempdir_factory, store, dep, +): + path = make_repo(tempdir_factory, 'rust_hooks_repo') + config = make_config_from_repo(path) + # A small rust package with no dependencies. + config['hooks'][0]['additional_dependencies'] = [dep] + repo = Repository.create(config, store) + repo.require_installed() + (prefix, _, _, _), = repo._venvs() + binaries = os.listdir(prefix.path( + helpers.environment_dir(rust.ENVIRONMENT_DIR, 'default'), 'bin', + )) + assert 'shellharden' in binaries + + +@pytest.mark.integration +def test_additional_rust_lib_dependencies_installed( + tempdir_factory, store, +): + path = make_repo(tempdir_factory, 'rust_hooks_repo') + config = make_config_from_repo(path) + # A small rust package with no dependencies. + deps = ['shellharden:3.1.0'] + config['hooks'][0]['additional_dependencies'] = deps + repo = Repository.create(config, store) + repo.require_installed() + (prefix, _, _, _), = repo._venvs() + binaries = os.listdir(prefix.path( + helpers.environment_dir(rust.ENVIRONMENT_DIR, 'default'), 'bin', + )) + assert 'rust-hello-world' in binaries + assert 'shellharden' not in binaries + + @pytest.mark.integration def test_missing_executable(tempdir_factory, store): _test_hook_repo( From 23fe0be2863e6cf13c1b1bcf830fb1047035df8c Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Tue, 22 May 2018 20:20:46 -0700 Subject: [PATCH 194/544] Add Rust to CI --- .travis.yml | 2 ++ appveyor.yml | 2 ++ tests/repository_test.py | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/.travis.yml b/.travis.yml index 8f91d702c..9327173f7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,6 +24,8 @@ before_install: fi - git --version - './get-swift.sh && export PATH="/tmp/swift/usr/bin:$PATH"' + - 'curl -sSf https://sh.rustup.rs | bash -s -- -y' + - export PATH="$HOME/.cargo/bin:$PATH" after_success: coveralls cache: directories: diff --git a/appveyor.yml b/appveyor.yml index ddb9af3cd..772caf4de 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -11,6 +11,8 @@ install: - pip install tox virtualenv --upgrade - "mkdir -p C:\\Temp" - "SET TMPDIR=C:\\Temp" + - "curl -sSf https://sh.rustup.rs | bash -s -- -y" + - "SET PATH=%USERPROFILE%\\.cargo\\bin;%PATH%" # Not a C# project build: false diff --git a/tests/repository_test.py b/tests/repository_test.py index 6fece071f..ba7be1fee 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -306,6 +306,8 @@ def test_additional_rust_cli_dependencies_installed( binaries = os.listdir(prefix.path( helpers.environment_dir(rust.ENVIRONMENT_DIR, 'default'), 'bin', )) + # normalize for windows + binaries = [os.path.splitext(binary)[0] for binary in binaries] assert 'shellharden' in binaries @@ -324,6 +326,8 @@ def test_additional_rust_lib_dependencies_installed( binaries = os.listdir(prefix.path( helpers.environment_dir(rust.ENVIRONMENT_DIR, 'default'), 'bin', )) + # normalize for windows + binaries = [os.path.splitext(binary)[0] for binary in binaries] assert 'rust-hello-world' in binaries assert 'shellharden' not in binaries From f57958a9d0454153219acbe2f711f170b9d88eb1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 23 May 2018 17:36:23 -0700 Subject: [PATCH 195/544] Remove (unused) Makefile Committed via https://github.com/asottile/all-repos --- Makefile | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 Makefile diff --git a/Makefile b/Makefile deleted file mode 100644 index 16868f1e8..000000000 --- a/Makefile +++ /dev/null @@ -1,26 +0,0 @@ -REBUILD_FLAG = - -.PHONY: all -all: venv test - -.PHONY: venv -venv: .venv.touch - tox -e venv $(REBUILD_FLAG) - -.PHONY: tests test -tests: test -test: .venv.touch - tox $(REBUILD_FLAG) - - -.venv.touch: setup.py requirements-dev.txt - $(eval REBUILD_FLAG := --recreate) - touch .venv.touch - - -.PHONY: clean -clean: - find . -name '*.pyc' -delete - rm -rf .tox - rm -rf ./venv-* - rm -f .venv.touch From 5ac2ba0f7b2139670a656b2d6030e3c083160de6 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Thu, 24 May 2018 19:42:58 -0700 Subject: [PATCH 196/544] Make local hooks work --- pre_commit/resources/empty_template/Cargo.toml | 7 +++++++ pre_commit/resources/empty_template/main.rs | 1 + tests/repository_test.py | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 pre_commit/resources/empty_template/Cargo.toml create mode 100644 pre_commit/resources/empty_template/main.rs diff --git a/pre_commit/resources/empty_template/Cargo.toml b/pre_commit/resources/empty_template/Cargo.toml new file mode 100644 index 000000000..3dfeffafb --- /dev/null +++ b/pre_commit/resources/empty_template/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "__fake_crate" +version = "0.0.0" + +[[bin]] +name = "__fake_cmd" +path = "main.rs" diff --git a/pre_commit/resources/empty_template/main.rs b/pre_commit/resources/empty_template/main.rs new file mode 100644 index 000000000..f328e4d9d --- /dev/null +++ b/pre_commit/resources/empty_template/main.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/tests/repository_test.py b/tests/repository_test.py index ba7be1fee..2ca399ce6 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -604,6 +604,24 @@ def test_local_golang_additional_dependencies(store): assert _norm_out(ret[1]) == b"Hello, Go examples!\n" +def test_local_rust_additional_dependencies(store): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'hello', + 'name': 'hello', + 'entry': 'hello', + 'language': 'rust', + 'additional_dependencies': ['cli:hello-cli:0.2.2'], + }], + } + repo = Repository.create(config, store) + (_, hook), = repo.hooks + ret = repo.run_hook(hook, ()) + assert ret[0] == 0 + assert _norm_out(ret[1]) == b"Hello World!\n" + + def test_reinstall(tempdir_factory, store, log_info_mock): path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) From d6f8ea8fd19213f760927360408f24298c0ec91e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 25 May 2018 15:00:32 -0700 Subject: [PATCH 197/544] pytest: drop the dot! Committed via https://github.com/asottile/all-repos --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e9a9f9e38..ad7bf01fc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,7 +26,7 @@ your changes immediately. ### Running a specific test Running a specific test with the environment activated is as easy as: -`py.test tests -k test_the_name_of_your_test` +`pytest tests -k test_the_name_of_your_test` ### Running all the tests @@ -36,7 +36,7 @@ significant cpu while running the slower node / ruby integration tests. Alternatively, with the environment activated you can run all of the tests using: -`py.test tests` +`pytest tests` ### Setting up the hooks From 97fb49a533de9a378d20f0a41e79df118362e534 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 26 May 2018 13:39:32 -0700 Subject: [PATCH 198/544] v1.10.0 --- CHANGELOG.md | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6beb98420..430917785 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +1.10.0 +====== + +### Features +- Add support for hooks written in `rust` + - #751 PR by @chriskuehl. + 1.9.0 ===== diff --git a/setup.py b/setup.py index 831dc0001..5b7ab3fb4 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.9.0', + version='1.10.0', author='Anthony Sottile', author_email='asottile@umich.edu', From 805a2921ad0d34698433972c6fcb1a6dca47191d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 28 May 2018 15:14:17 -0700 Subject: [PATCH 199/544] Invoke -mvenv with the original python if in a -mvirtualenv venv --- pre_commit/languages/python_venv.py | 34 ++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/python_venv.py b/pre_commit/languages/python_venv.py index 20613a49b..4397ce183 100644 --- a/pre_commit/languages/python_venv.py +++ b/pre_commit/languages/python_venv.py @@ -1,14 +1,46 @@ from __future__ import unicode_literals +import os.path + from pre_commit.languages import python +from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output ENVIRONMENT_DIR = 'py_venv' +def orig_py_exe(exe): # pragma: no cover (platform specific) + """A -mvenv virtualenv made from a -mvirtualenv virtualenv installs + packages to the incorrect location. Attempt to find the _original_ exe + and invoke `-mvenv` from there. + + See: + - https://github.com/pre-commit/pre-commit/issues/755 + - https://github.com/pypa/virtualenv/issues/1095 + - https://bugs.python.org/issue30811 + """ + try: + prefix_script = 'import sys; print(sys.real_prefix)' + _, prefix, _ = cmd_output(exe, '-c', prefix_script) + prefix = prefix.strip() + except CalledProcessError: + # not created from -mvirtualenv + return exe + + if os.name == 'nt': + expected = os.path.join(prefix, 'python.exe') + else: + expected = os.path.join(prefix, 'bin', os.path.basename(exe)) + + if os.path.exists(expected): + return expected + else: + return exe + + def make_venv(envdir, python): - cmd_output(python, '-mvenv', envdir, cwd='/') + cmd_output(orig_py_exe(python), '-mvenv', envdir, cwd='/') get_default_version = python.get_default_version From cf5f8406a1593aa31dcfa5fb2a3766a9ff3f8a96 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 28 May 2018 17:16:52 -0700 Subject: [PATCH 200/544] v1.10.1 --- CHANGELOG.md | 8 ++++++++ setup.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 430917785..8895d6efd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +1.10.1 +====== + +### Fixes +- `python_venv` language would leak dependencies when pre-commit was installed + in a `-mvirtualenv` virtualenv + - #755 #756 issue and PR by @asottile. + 1.10.0 ====== diff --git a/setup.py b/setup.py index 5b7ab3fb4..9b894988e 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.10.0', + version='1.10.1', author='Anthony Sottile', author_email='asottile@umich.edu', From 3b1b9ac4cf6c5e07e01919ec8b1d703a95c4a43b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 1 Jun 2018 13:39:40 -0700 Subject: [PATCH 201/544] E309 is no longer rewritten by autopep8 Committed via https://github.com/asottile/all-repos --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index a254c3698..15674934c 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,7 @@ envdir = venv-{[tox]project} commands = [pep8] -ignore = E265,E309,E501 +ignore = E265,E501 [pytest] env = From ba97b66055f07dd43adaf70c4c4e5ccdd2721941 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 1 Jun 2018 17:55:12 -0700 Subject: [PATCH 202/544] Include README as long_description --- .travis.yml | 2 +- setup.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9327173f7..29b8a04b5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ matrix: - env: TOXENV=py36 python: 3.6 - env: TOXENV=pypy - python: pypy-5.7.1 + python: pypy2.7-5.10.0 install: pip install coveralls tox script: tox before_install: diff --git a/setup.py b/setup.py index 9b894988e..211c89886 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,8 @@ from setuptools import find_packages from setuptools import setup +with open('README.md') as f: + long_description = f.read() setup( name='pre_commit', @@ -8,12 +10,12 @@ 'A framework for managing and maintaining multi-language pre-commit ' 'hooks.' ), + long_description=long_description, + long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', version='1.10.1', - author='Anthony Sottile', author_email='asottile@umich.edu', - classifiers=[ 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 2', @@ -24,7 +26,6 @@ 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', ], - packages=find_packages(exclude=('tests*', 'testing*')), package_data={ 'pre_commit': [ From 51cf46e66045162c2c827cb3c05661f46156eb4a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 3 Jun 2018 20:56:07 -0700 Subject: [PATCH 203/544] Simplify setup.py in arbitrary_bytes_repo --- .../{python3_hook/main.py => python3_hook.py} | 2 +- .../arbitrary_bytes_repo/python3_hook/__init__.py | 0 testing/resources/arbitrary_bytes_repo/setup.py | 7 ++----- 3 files changed, 3 insertions(+), 6 deletions(-) rename testing/resources/arbitrary_bytes_repo/{python3_hook/main.py => python3_hook.py} (96%) delete mode 100644 testing/resources/arbitrary_bytes_repo/python3_hook/__init__.py diff --git a/testing/resources/arbitrary_bytes_repo/python3_hook/main.py b/testing/resources/arbitrary_bytes_repo/python3_hook.py similarity index 96% rename from testing/resources/arbitrary_bytes_repo/python3_hook/main.py rename to testing/resources/arbitrary_bytes_repo/python3_hook.py index c6a5547cb..ba698a934 100644 --- a/testing/resources/arbitrary_bytes_repo/python3_hook/main.py +++ b/testing/resources/arbitrary_bytes_repo/python3_hook.py @@ -5,7 +5,7 @@ import sys -def func(): +def main(): # Intentionally write mixed encoding to the output. This should not crash # pre-commit and should write bytes to the output. sys.stdout.buffer.write('☃'.encode('UTF-8') + '²'.encode('latin1') + b'\n') diff --git a/testing/resources/arbitrary_bytes_repo/python3_hook/__init__.py b/testing/resources/arbitrary_bytes_repo/python3_hook/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/testing/resources/arbitrary_bytes_repo/setup.py b/testing/resources/arbitrary_bytes_repo/setup.py index bf7690c03..c780e427a 100644 --- a/testing/resources/arbitrary_bytes_repo/setup.py +++ b/testing/resources/arbitrary_bytes_repo/setup.py @@ -1,11 +1,8 @@ -from setuptools import find_packages from setuptools import setup setup( name='python3_hook', version='0.0.0', - packages=find_packages('.'), - entry_points={ - 'console_scripts': ['python3-hook = python3_hook.main:func'], - }, + py_modules=['python3_hook'], + entry_points={'console_scripts': ['python3-hook=python3_hook:main']}, ) From 37f49d8fd447e7ed9bcb12d07822feb19d6dd06f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 3 Jun 2018 21:33:10 -0700 Subject: [PATCH 204/544] Stop crlf messages in appveyor --- appveyor.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/appveyor.yml b/appveyor.yml index 772caf4de..271edafaf 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -19,6 +19,7 @@ build: false before_test: # Shut up CRLF messages + - git config --global core.autocrlf false - git config --global core.safecrlf false test_script: tox From f0842429b94219a868f4a294639b78459d0948ef Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 5 Jun 2018 18:26:02 -0700 Subject: [PATCH 205/544] Move testing scripts into testing --- .travis.yml | 4 ++-- get-swift.sh => testing/get-swift.sh | 0 latest-git.sh => testing/latest-git.sh | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename get-swift.sh => testing/get-swift.sh (100%) rename latest-git.sh => testing/latest-git.sh (100%) diff --git a/.travis.yml b/.travis.yml index 29b8a04b5..84fd3f7d9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,11 +19,11 @@ before_install: - git --version - | if [ "$LATEST_GIT" = "1" ]; then - ./latest-git.sh + testing/latest-git.sh export PATH="/tmp/git/bin:$PATH" fi - git --version - - './get-swift.sh && export PATH="/tmp/swift/usr/bin:$PATH"' + - 'testing/get-swift.sh && export PATH="/tmp/swift/usr/bin:$PATH"' - 'curl -sSf https://sh.rustup.rs | bash -s -- -y' - export PATH="$HOME/.cargo/bin:$PATH" after_success: coveralls diff --git a/get-swift.sh b/testing/get-swift.sh similarity index 100% rename from get-swift.sh rename to testing/get-swift.sh diff --git a/latest-git.sh b/testing/latest-git.sh similarity index 100% rename from latest-git.sh rename to testing/latest-git.sh From 5b6a5abae940c147ad294409194ef53e40a0aac3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 11 Jun 2018 12:49:45 -0700 Subject: [PATCH 206/544] Consistent ordering of filenames --- pre_commit/commands/run.py | 4 ++-- tests/commands/run_test.py | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 98ae25dc8..a07256603 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -38,14 +38,14 @@ def _hook_msg_start(hook, verbose): def _filter_by_include_exclude(filenames, include, exclude): include_re, exclude_re = re.compile(include), re.compile(exclude) - return { + return [ filename for filename in filenames if ( include_re.search(filename) and not exclude_re.search(filename) and os.path.lexists(filename) ) - } + ] def _filter_by_types(filenames, types, exclude_types): diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index cd32c5f65..91e84d98c 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -770,19 +770,19 @@ def test_fail_fast( def some_filenames(): return ( '.pre-commit-hooks.yaml', - 'pre_commit/main.py', - 'pre_commit/git.py', 'im_a_file_that_doesnt_exist.py', + 'pre_commit/git.py', + 'pre_commit/main.py', ) def test_include_exclude_base_case(some_filenames): ret = _filter_by_include_exclude(some_filenames, '', '^$') - assert ret == { + assert ret == [ '.pre-commit-hooks.yaml', - 'pre_commit/main.py', 'pre_commit/git.py', - } + 'pre_commit/main.py', + ] @xfailif_no_symlink @@ -790,19 +790,19 @@ def test_matches_broken_symlink(tmpdir): # pragma: no cover (non-windows) with tmpdir.as_cwd(): os.symlink('does-not-exist', 'link') ret = _filter_by_include_exclude({'link'}, '', '^$') - assert ret == {'link'} + assert ret == ['link'] def test_include_exclude_total_match(some_filenames): ret = _filter_by_include_exclude(some_filenames, r'^.*\.py$', '^$') - assert ret == {'pre_commit/main.py', 'pre_commit/git.py'} + assert ret == ['pre_commit/git.py', 'pre_commit/main.py'] def test_include_exclude_does_search_instead_of_match(some_filenames): ret = _filter_by_include_exclude(some_filenames, r'\.yaml$', '^$') - assert ret == {'.pre-commit-hooks.yaml'} + assert ret == ['.pre-commit-hooks.yaml'] def test_include_exclude_exclude_removes_files(some_filenames): ret = _filter_by_include_exclude(some_filenames, '', r'\.py$') - assert ret == {'.pre-commit-hooks.yaml'} + assert ret == ['.pre-commit-hooks.yaml'] From a12feebf4bf7941a6c0d084a1364d8186bcddb29 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 11 Jun 2018 13:29:17 -0700 Subject: [PATCH 207/544] v1.10.2 --- CHANGELOG.md | 8 ++++++++ setup.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8895d6efd..932e4b0e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +1.10.2 +====== + +### Fixes +- pre-commit now invokes hooks with a consistent ordering of filenames + - issue by @mxr. + - #767 PR by @asottile. + 1.10.1 ====== diff --git a/setup.py b/setup.py index 211c89886..4f0b897fc 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.10.1', + version='1.10.2', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From 37c94bbe714aa5ac7b17ac8e54a290721262c625 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 17 Jun 2018 23:51:03 -0700 Subject: [PATCH 208/544] Fix invalid escape sequences --- testing/util.py | 2 +- tests/commands/try_repo_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/util.py b/testing/util.py index 42cf07eb4..6a66c7c9a 100644 --- a/testing/util.py +++ b/testing/util.py @@ -53,7 +53,7 @@ def broken_deep_listdir(): # pragma: no cover (platform specific) except OSError: return True try: - os.listdir(b'\\\\?\C:' + b'\\' * 300) + os.listdir(b'\\\\?\\C:' + b'\\' * 300) except TypeError: return True except OSError: diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index 490cdd563..66d1642df 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -19,7 +19,7 @@ def try_repo_opts(repo, ref=None, **kwargs): def _get_out(cap_out): out = cap_out.get().replace('\r\n', '\n') - out = re.sub('\[INFO\].+\n', '', out) + out = re.sub(r'\[INFO\].+\n', '', out) start, using_config, config, rest = out.split('=' * 79 + '\n') assert start == '' assert using_config == 'Using config:\n' From 0e430be0cee51c6071da16b95574e67c3663db2b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 29 Jun 2018 20:04:16 -0700 Subject: [PATCH 209/544] autoupdate: separate store from runner --- pre_commit/commands/autoupdate.py | 10 ++-- pre_commit/main.py | 2 +- tests/commands/autoupdate_test.py | 84 +++++++++++++------------------ 3 files changed, 42 insertions(+), 54 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index f4ce67508..241126ddf 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -24,7 +24,7 @@ class RepositoryCannotBeUpdatedError(RuntimeError): pass -def _update_repo(repo_config, runner, tags_only): +def _update_repo(repo_config, store, tags_only): """Updates a repository to the tip of `master`. If the repository cannot be updated because a hook that is configured does not exist in `master`, this raises a RepositoryCannotBeUpdatedError @@ -32,7 +32,7 @@ def _update_repo(repo_config, runner, tags_only): Args: repo_config - A config for a repository """ - repo_path = runner.store.clone(repo_config['repo'], repo_config['rev']) + repo_path = store.clone(repo_config['repo'], repo_config['rev']) cmd_output('git', 'fetch', cwd=repo_path) tag_cmd = ('git', 'describe', 'origin/master', '--tags') @@ -53,7 +53,7 @@ def _update_repo(repo_config, runner, tags_only): # Construct a new config with the head rev new_config = OrderedDict(repo_config) new_config['rev'] = rev - new_repo = Repository.create(new_config, runner.store) + new_repo = Repository.create(new_config, store) # See if any of our hooks were deleted with the new commits hooks = {hook['id'] for hook in repo_config['hooks']} @@ -105,7 +105,7 @@ def _write_new_config_file(path, output): f.write(to_write) -def autoupdate(runner, tags_only, repos=()): +def autoupdate(runner, store, tags_only, repos=()): """Auto-update the pre-commit config to the latest versions of repos.""" migrate_config(runner, quiet=True) retv = 0 @@ -125,7 +125,7 @@ def autoupdate(runner, tags_only, repos=()): continue output.write('Updating {}...'.format(repo_config['repo'])) try: - new_repo_config = _update_repo(repo_config, runner, tags_only) + new_repo_config = _update_repo(repo_config, store, tags_only) except RepositoryCannotBeUpdatedError as error: output.write_line(error.args[0]) output_repos.append(repo_config) diff --git a/pre_commit/main.py b/pre_commit/main.py index 9b7f14168..f98823685 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -248,7 +248,7 @@ def main(argv=None): if args.tags_only: logger.warning('--tags-only is the default') return autoupdate( - runner, + runner, runner.store, tags_only=not args.bleeding_edge, repos=args.repos, ) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 3e268c342..5408d45ae 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -30,31 +30,27 @@ def up_to_date_repo(tempdir_factory): yield make_repo(tempdir_factory, 'python_hooks_repo') -def test_up_to_date_repo(up_to_date_repo, runner_with_mocked_store): +def test_up_to_date_repo(up_to_date_repo, store): config = make_config_from_repo(up_to_date_repo) input_rev = config['rev'] - ret = _update_repo(config, runner_with_mocked_store, tags_only=False) + ret = _update_repo(config, store, tags_only=False) assert ret['rev'] == input_rev -def test_autoupdate_up_to_date_repo( - up_to_date_repo, in_tmpdir, mock_out_store_directory, -): +def test_autoupdate_up_to_date_repo(up_to_date_repo, in_tmpdir, store): # Write out the config config = make_config_from_repo(up_to_date_repo, check=False) write_config('.', config) before = open(C.CONFIG_FILE).read() assert '^$' not in before - ret = autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) + ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) after = open(C.CONFIG_FILE).read() assert ret == 0 assert before == after -def test_autoupdate_old_revision_broken( - tempdir_factory, in_tmpdir, mock_out_store_directory, -): +def test_autoupdate_old_revision_broken(tempdir_factory, in_tmpdir, store): """In $FUTURE_VERSION, hooks.yaml will no longer be supported. This asserts that when that day comes, pre-commit will be able to autoupdate despite not being able to read hooks.yaml in that repository. @@ -73,7 +69,7 @@ def test_autoupdate_old_revision_broken( config['rev'] = rev write_config('.', config) before = open(C.CONFIG_FILE).read() - ret = autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) + ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) after = open(C.CONFIG_FILE).read() assert ret == 0 assert before != after @@ -94,18 +90,16 @@ def out_of_date_repo(tempdir_factory): ) -def test_out_of_date_repo(out_of_date_repo, runner_with_mocked_store): +def test_out_of_date_repo(out_of_date_repo, store): config = make_config_from_repo( out_of_date_repo.path, rev=out_of_date_repo.original_rev, ) - ret = _update_repo(config, runner_with_mocked_store, tags_only=False) + ret = _update_repo(config, store, tags_only=False) assert ret['rev'] != out_of_date_repo.original_rev assert ret['rev'] == out_of_date_repo.head_rev -def test_autoupdate_out_of_date_repo( - out_of_date_repo, in_tmpdir, mock_out_store_directory, -): +def test_autoupdate_out_of_date_repo(out_of_date_repo, in_tmpdir, store): # Write out the config config = make_config_from_repo( out_of_date_repo.path, rev=out_of_date_repo.original_rev, check=False, @@ -113,7 +107,7 @@ def test_autoupdate_out_of_date_repo( write_config('.', config) before = open(C.CONFIG_FILE).read() - ret = autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) + ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) after = open(C.CONFIG_FILE).read() assert ret == 0 assert before != after @@ -123,7 +117,7 @@ def test_autoupdate_out_of_date_repo( def test_autoupdate_out_of_date_repo_with_correct_repo_name( - out_of_date_repo, in_tmpdir, mock_out_store_directory, + out_of_date_repo, in_tmpdir, store, ): stale_config = make_config_from_repo( out_of_date_repo.path, rev=out_of_date_repo.original_rev, check=False, @@ -136,7 +130,7 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( runner = Runner('.', C.CONFIG_FILE) before = open(C.CONFIG_FILE).read() repo_name = 'file://{}'.format(out_of_date_repo.path) - ret = autoupdate(runner, tags_only=False, repos=(repo_name,)) + ret = autoupdate(runner, store, tags_only=False, repos=(repo_name,)) after = open(C.CONFIG_FILE).read() assert ret == 0 assert before != after @@ -145,7 +139,7 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( def test_autoupdate_out_of_date_repo_with_wrong_repo_name( - out_of_date_repo, in_tmpdir, mock_out_store_directory, + out_of_date_repo, in_tmpdir, store, ): # Write out the config config = make_config_from_repo( @@ -156,15 +150,13 @@ def test_autoupdate_out_of_date_repo_with_wrong_repo_name( runner = Runner('.', C.CONFIG_FILE) before = open(C.CONFIG_FILE).read() # It will not update it, because the name doesn't match - ret = autoupdate(runner, tags_only=False, repos=('wrong_repo_name',)) + ret = autoupdate(runner, store, tags_only=False, repos=('dne',)) after = open(C.CONFIG_FILE).read() assert ret == 0 assert before == after -def test_does_not_reformat( - out_of_date_repo, mock_out_store_directory, in_tmpdir, -): +def test_does_not_reformat(in_tmpdir, out_of_date_repo, store): fmt = ( 'repos:\n' '- repo: {}\n' @@ -178,14 +170,14 @@ def test_does_not_reformat( with open(C.CONFIG_FILE, 'w') as f: f.write(config) - autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) + autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) after = open(C.CONFIG_FILE).read() expected = fmt.format(out_of_date_repo.path, out_of_date_repo.head_rev) assert after == expected def test_loses_formatting_when_not_detectable( - out_of_date_repo, mock_out_store_directory, in_tmpdir, + out_of_date_repo, store, in_tmpdir, ): """A best-effort attempt is made at updating rev without rewriting formatting. When the original formatting cannot be detected, this @@ -207,7 +199,7 @@ def test_loses_formatting_when_not_detectable( with open(C.CONFIG_FILE, 'w') as f: f.write(config) - autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) + autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) after = open(C.CONFIG_FILE).read() expected = ( 'repos:\n' @@ -225,15 +217,13 @@ def tagged_repo(out_of_date_repo): yield out_of_date_repo -def test_autoupdate_tagged_repo( - tagged_repo, in_tmpdir, mock_out_store_directory, -): +def test_autoupdate_tagged_repo(tagged_repo, in_tmpdir, store): config = make_config_from_repo( tagged_repo.path, rev=tagged_repo.original_rev, ) write_config('.', config) - ret = autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) + ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) assert ret == 0 assert 'v1.2.3' in open(C.CONFIG_FILE).read() @@ -244,16 +234,14 @@ def tagged_repo_with_more_commits(tagged_repo): yield tagged_repo -def test_autoupdate_tags_only( - tagged_repo_with_more_commits, in_tmpdir, mock_out_store_directory, -): +def test_autoupdate_tags_only(tagged_repo_with_more_commits, in_tmpdir, store): config = make_config_from_repo( tagged_repo_with_more_commits.path, rev=tagged_repo_with_more_commits.original_rev, ) write_config('.', config) - ret = autoupdate(Runner('.', C.CONFIG_FILE), tags_only=True) + ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=True) assert ret == 0 assert 'v1.2.3' in open(C.CONFIG_FILE).read() @@ -273,20 +261,18 @@ def hook_disappearing_repo(tempdir_factory): yield auto_namedtuple(path=path, original_rev=original_rev) -def test_hook_disppearing_repo_raises( - hook_disappearing_repo, runner_with_mocked_store, -): +def test_hook_disppearing_repo_raises(hook_disappearing_repo, store): config = make_config_from_repo( hook_disappearing_repo.path, rev=hook_disappearing_repo.original_rev, hooks=[OrderedDict((('id', 'foo'),))], ) with pytest.raises(RepositoryCannotBeUpdatedError): - _update_repo(config, runner_with_mocked_store, tags_only=False) + _update_repo(config, store, tags_only=False) def test_autoupdate_hook_disappearing_repo( - hook_disappearing_repo, in_tmpdir, mock_out_store_directory, + hook_disappearing_repo, in_tmpdir, store, ): config = make_config_from_repo( hook_disappearing_repo.path, @@ -297,25 +283,25 @@ def test_autoupdate_hook_disappearing_repo( write_config('.', config) before = open(C.CONFIG_FILE).read() - ret = autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) + ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) after = open(C.CONFIG_FILE).read() assert ret == 1 assert before == after -def test_autoupdate_local_hooks(tempdir_factory): +def test_autoupdate_local_hooks(tempdir_factory, store): git_path = git_dir(tempdir_factory) config = config_with_local_hooks() path = add_config_to_repo(git_path, config) runner = Runner(path, C.CONFIG_FILE) - assert autoupdate(runner, tags_only=False) == 0 + assert autoupdate(runner, store, tags_only=False) == 0 new_config_writen = load_config(runner.config_file_path) assert len(new_config_writen['repos']) == 1 assert new_config_writen['repos'][0] == config def test_autoupdate_local_hooks_with_out_of_date_repo( - out_of_date_repo, in_tmpdir, mock_out_store_directory, + out_of_date_repo, in_tmpdir, store, ): stale_config = make_config_from_repo( out_of_date_repo.path, rev=out_of_date_repo.original_rev, check=False, @@ -324,13 +310,13 @@ def test_autoupdate_local_hooks_with_out_of_date_repo( config = {'repos': [local_config, stale_config]} write_config('.', config) runner = Runner('.', C.CONFIG_FILE) - assert autoupdate(runner, tags_only=False) == 0 + assert autoupdate(runner, store, tags_only=False) == 0 new_config_writen = load_config(runner.config_file_path) assert len(new_config_writen['repos']) == 2 assert new_config_writen['repos'][0] == local_config -def test_autoupdate_meta_hooks(tmpdir, capsys): +def test_autoupdate_meta_hooks(tmpdir, capsys, store): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write( 'repos:\n' @@ -338,7 +324,8 @@ def test_autoupdate_meta_hooks(tmpdir, capsys): ' hooks:\n' ' - id: check-useless-excludes\n', ) - ret = autoupdate(Runner(tmpdir.strpath, C.CONFIG_FILE), tags_only=True) + runner = Runner(tmpdir.strpath, C.CONFIG_FILE) + ret = autoupdate(runner, store, tags_only=True) assert ret == 0 assert cfg.read() == ( 'repos:\n' @@ -348,7 +335,7 @@ def test_autoupdate_meta_hooks(tmpdir, capsys): ) -def test_updates_old_format_to_new_format(tmpdir, capsys): +def test_updates_old_format_to_new_format(tmpdir, capsys, store): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write( '- repo: local\n' @@ -358,7 +345,8 @@ def test_updates_old_format_to_new_format(tmpdir, capsys): ' entry: ./bin/foo.sh\n' ' language: script\n', ) - ret = autoupdate(Runner(tmpdir.strpath, C.CONFIG_FILE), tags_only=True) + runner = Runner(tmpdir.strpath, C.CONFIG_FILE) + ret = autoupdate(runner, store, tags_only=True) assert ret == 0 contents = cfg.read() assert contents == ( From 6d683a5fac0a63662c24c8236218761c6831f90f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 29 Jun 2018 20:08:23 -0700 Subject: [PATCH 210/544] clean: separate store from runner --- pre_commit/commands/clean.py | 4 ++-- pre_commit/main.py | 2 +- tests/commands/clean_test.py | 20 +++++++++----------- tests/conftest.py | 7 ------- 4 files changed, 12 insertions(+), 21 deletions(-) diff --git a/pre_commit/commands/clean.py b/pre_commit/commands/clean.py index 75d0acc03..5c7630292 100644 --- a/pre_commit/commands/clean.py +++ b/pre_commit/commands/clean.py @@ -7,9 +7,9 @@ from pre_commit.util import rmtree -def clean(runner): +def clean(store): legacy_path = os.path.expanduser('~/.pre-commit') - for directory in (runner.store.directory, legacy_path): + for directory in (store.directory, legacy_path): if os.path.exists(directory): rmtree(directory) output.write_line('Cleaned {}.'.format(directory)) diff --git a/pre_commit/main.py b/pre_commit/main.py index f98823685..4ff8073d7 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -243,7 +243,7 @@ def main(argv=None): elif args.command == 'uninstall': return uninstall(runner, hook_type=args.hook_type) elif args.command == 'clean': - return clean(runner) + return clean(runner.store) elif args.command == 'autoupdate': if args.tags_only: logger.warning('--tags-only is the default') diff --git a/tests/commands/clean_test.py b/tests/commands/clean_test.py index fddd444d9..3bfa46a3f 100644 --- a/tests/commands/clean_test.py +++ b/tests/commands/clean_test.py @@ -6,7 +6,6 @@ import pytest from pre_commit.commands.clean import clean -from pre_commit.util import rmtree @pytest.fixture(autouse=True) @@ -21,17 +20,16 @@ def _expanduser(path, *args, **kwargs): yield fake_old_dir -def test_clean(runner_with_mocked_store, fake_old_dir): +def test_clean(store, fake_old_dir): + store.require_created() assert os.path.exists(fake_old_dir) - assert os.path.exists(runner_with_mocked_store.store.directory) - clean(runner_with_mocked_store) + assert os.path.exists(store.directory) + clean(store) assert not os.path.exists(fake_old_dir) - assert not os.path.exists(runner_with_mocked_store.store.directory) + assert not os.path.exists(store.directory) -def test_clean_empty(runner_with_mocked_store): - """Make sure clean succeeds when the directory doesn't exist.""" - rmtree(runner_with_mocked_store.store.directory) - assert not os.path.exists(runner_with_mocked_store.store.directory) - clean(runner_with_mocked_store) - assert not os.path.exists(runner_with_mocked_store.store.directory) +def test_clean_idempotent(store): + assert not os.path.exists(store.directory) + clean(store) + assert not os.path.exists(store.directory) diff --git a/tests/conftest.py b/tests/conftest.py index c0e13186e..eb2ecf6e7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,10 +11,8 @@ import pytest import six -import pre_commit.constants as C from pre_commit import output from pre_commit.logging_handler import add_logging_handler -from pre_commit.runner import Runner from pre_commit.store import Store from pre_commit.util import cmd_output from testing.fixtures import git_dir @@ -151,11 +149,6 @@ def store(tempdir_factory): yield Store(os.path.join(tempdir_factory.get(), '.pre-commit')) -@pytest.fixture -def runner_with_mocked_store(mock_out_store_directory): - yield Runner('/', C.CONFIG_FILE) - - @pytest.fixture def log_info_mock(): with mock.patch.object(logging.getLogger('pre_commit'), 'info') as mck: From c01ffc83f88e792b1c91116175c209f83413b67a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 29 Jun 2018 22:35:53 -0700 Subject: [PATCH 211/544] Separate store from runner --- pre_commit/commands/install_uninstall.py | 9 +- pre_commit/commands/run.py | 7 +- pre_commit/commands/try_repo.py | 6 +- pre_commit/main.py | 13 +- pre_commit/meta_hooks/check_hooks_apply.py | 7 +- pre_commit/repository.py | 4 + pre_commit/runner.py | 15 +- pre_commit/store.py | 5 +- tests/commands/install_uninstall_test.py | 167 ++++++++------- tests/commands/run_test.py | 232 +++++++++------------ tests/conftest.py | 2 +- tests/error_handler_test.py | 6 +- tests/main_test.py | 10 +- tests/meta_hooks/check_hooks_apply_test.py | 18 +- tests/runner_test.py | 74 ------- 15 files changed, 228 insertions(+), 347 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 919122265..6b2d16f5c 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -7,6 +7,7 @@ import sys from pre_commit import output +from pre_commit.repository import repositories from pre_commit.util import cmd_output from pre_commit.util import make_executable from pre_commit.util import mkdirp @@ -36,7 +37,7 @@ def is_our_script(filename): def install( - runner, overwrite=False, hooks=False, hook_type='pre-commit', + runner, store, overwrite=False, hooks=False, hook_type='pre-commit', skip_on_missing_conf=False, ): """Install the pre-commit hooks.""" @@ -89,13 +90,13 @@ def install( # If they requested we install all of the hooks, do so. if hooks: - install_hooks(runner) + install_hooks(runner, store) return 0 -def install_hooks(runner): - for repository in runner.repositories: +def install_hooks(runner, store): + for repository in repositories(runner.config, store): repository.require_installed() diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index a07256603..b5dcc1e28 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -13,6 +13,7 @@ from pre_commit import git from pre_commit import output from pre_commit.output import get_hook_message +from pre_commit.repository import repositories from pre_commit.staged_files_only import staged_files_only from pre_commit.util import cmd_output from pre_commit.util import memoize_by_cwd @@ -223,7 +224,7 @@ def _has_unstaged_config(runner): return retcode == 1 -def run(runner, args, environ=os.environ): +def run(runner, store, args, environ=os.environ): no_stash = args.all_files or bool(args.files) # Check if we have unresolved merge conflict files and fail fast. @@ -248,11 +249,11 @@ def run(runner, args, environ=os.environ): if no_stash: ctx = noop_context() else: - ctx = staged_files_only(runner.store.directory) + ctx = staged_files_only(store.directory) with ctx: repo_hooks = [] - for repo in runner.repositories: + for repo in repositories(runner.config, store): for _, hook in repo.hooks: if ( (not args.hook or hook['id'] == args.hook) and diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index 681543169..431db1413 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -20,10 +20,11 @@ def try_repo(args): ref = args.ref or git.head_rev(args.repo) with tmpdir() as tempdir: + store = Store(tempdir) if args.hook: hooks = [{'id': args.hook}] else: - repo_path = Store(tempdir).clone(args.repo, ref) + repo_path = store.clone(args.repo, ref) manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) manifest = sorted(manifest, key=lambda hook: hook['id']) hooks = [{'id': hook['id']} for hook in manifest] @@ -42,5 +43,4 @@ def try_repo(args): output.write(config_s) output.write_line('=' * 79) - runner = Runner('.', config_filename, store_dir=tempdir) - return run(runner, args) + return run(Runner('.', config_filename), store, args) diff --git a/pre_commit/main.py b/pre_commit/main.py index 4ff8073d7..fafe36b12 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -21,6 +21,7 @@ from pre_commit.error_handler import error_handler from pre_commit.logging_handler import add_logging_handler from pre_commit.runner import Runner +from pre_commit.store import Store logger = logging.getLogger('pre_commit') @@ -230,32 +231,34 @@ def main(argv=None): with error_handler(): add_logging_handler(args.color) runner = Runner.create(args.config) + store = Store() git.check_for_cygwin_mismatch() if args.command == 'install': return install( - runner, overwrite=args.overwrite, hooks=args.install_hooks, + runner, store, + overwrite=args.overwrite, hooks=args.install_hooks, hook_type=args.hook_type, skip_on_missing_conf=args.allow_missing_config, ) elif args.command == 'install-hooks': - return install_hooks(runner) + return install_hooks(runner, store) elif args.command == 'uninstall': return uninstall(runner, hook_type=args.hook_type) elif args.command == 'clean': - return clean(runner.store) + return clean(store) elif args.command == 'autoupdate': if args.tags_only: logger.warning('--tags-only is the default') return autoupdate( - runner, runner.store, + runner, store, tags_only=not args.bleeding_edge, repos=args.repos, ) elif args.command == 'migrate-config': return migrate_config(runner) elif args.command == 'run': - return run(runner, args) + return run(runner, store, args) elif args.command == 'sample-config': return sample_config() elif args.command == 'try-repo': diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index 20d7f069f..23420f468 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -2,17 +2,18 @@ import pre_commit.constants as C from pre_commit import git +from pre_commit.clientlib import load_config from pre_commit.commands.run import _filter_by_include_exclude from pre_commit.commands.run import _filter_by_types -from pre_commit.runner import Runner +from pre_commit.repository import repositories +from pre_commit.store import Store def check_all_hooks_match_files(config_file): - runner = Runner.create(config_file) files = git.get_all_files() retv = 0 - for repo in runner.repositories: + for repo in repositories(load_config(config_file), Store()): for hook_id, hook in repo.hooks: if hook['always_run']: continue diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 0647d9df7..0f12bd9e8 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -282,3 +282,7 @@ def hooks(self): (hook['id'], _hook(self.manifest_hooks[hook['id']], hook)) for hook in self.repo_config['hooks'] ) + + +def repositories(config, store): + return tuple(Repository.create(x, store) for x in config['repos']) diff --git a/pre_commit/runner.py b/pre_commit/runner.py index 420c62df9..a6d0f576a 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -6,8 +6,6 @@ from pre_commit import git from pre_commit.clientlib import load_config -from pre_commit.repository import Repository -from pre_commit.store import Store class Runner(object): @@ -15,10 +13,9 @@ class Runner(object): repository under test. """ - def __init__(self, git_root, config_file, store_dir=None): + def __init__(self, git_root, config_file): self.git_root = git_root self.config_file = config_file - self._store_dir = store_dir @classmethod def create(cls, config_file): @@ -42,12 +39,6 @@ def config_file_path(self): def config(self): return load_config(self.config_file_path) - @cached_property - def repositories(self): - """Returns a tuple of the configured repositories.""" - repos = self.config['repos'] - return tuple(Repository.create(x, self.store) for x in repos) - def get_hook_path(self, hook_type): return os.path.join(self.git_dir, 'hooks', hook_type) @@ -58,7 +49,3 @@ def pre_commit_path(self): @cached_property def pre_push_path(self): return self.get_hook_path('pre-push') - - @cached_property - def store(self): - return Store(self._store_dir) diff --git a/pre_commit/store.py b/pre_commit/store.py index 8251e21b8..0ca6b7060 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -39,10 +39,7 @@ class Store(object): __created = False def __init__(self, directory=None): - if directory is None: - directory = self.get_default_directory() - - self.directory = directory + self.directory = directory or Store.get_default_directory() @contextlib.contextmanager def exclusive_lock(self): diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 491495f36..83ea38d32 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -45,44 +45,44 @@ def test_is_previous_pre_commit(tmpdir): assert is_our_script(f.strpath) -def test_install_pre_commit(tempdir_factory): +def test_install_pre_commit(tempdir_factory, store): path = git_dir(tempdir_factory) runner = Runner(path, C.CONFIG_FILE) - assert not install(runner) + assert not install(runner, store) assert os.access(runner.pre_commit_path, os.X_OK) - assert not install(runner, hook_type='pre-push') + assert not install(runner, store, hook_type='pre-push') assert os.access(runner.pre_push_path, os.X_OK) -def test_install_hooks_directory_not_present(tempdir_factory): +def test_install_hooks_directory_not_present(tempdir_factory, store): path = git_dir(tempdir_factory) # Simulate some git clients which don't make .git/hooks #234 hooks = os.path.join(path, '.git', 'hooks') if os.path.exists(hooks): # pragma: no cover (latest git) shutil.rmtree(hooks) runner = Runner(path, C.CONFIG_FILE) - install(runner) + install(runner, store) assert os.path.exists(runner.pre_commit_path) -def test_install_refuses_core_hookspath(tempdir_factory): +def test_install_refuses_core_hookspath(tempdir_factory, store): path = git_dir(tempdir_factory) with cwd(path): cmd_output('git', 'config', '--local', 'core.hooksPath', 'hooks') runner = Runner(path, C.CONFIG_FILE) - assert install(runner) + assert install(runner, store) @xfailif_no_symlink def test_install_hooks_dead_symlink( - tempdir_factory, + tempdir_factory, store, ): # pragma: no cover (non-windows) path = git_dir(tempdir_factory) runner = Runner(path, C.CONFIG_FILE) mkdirp(os.path.dirname(runner.pre_commit_path)) os.symlink('/fake/baz', os.path.join(path, '.git', 'hooks', 'pre-commit')) - install(runner) + install(runner, store) assert os.path.exists(runner.pre_commit_path) @@ -93,11 +93,11 @@ def test_uninstall_does_not_blow_up_when_not_there(tempdir_factory): assert ret == 0 -def test_uninstall(tempdir_factory): +def test_uninstall(tempdir_factory, store): path = git_dir(tempdir_factory) runner = Runner(path, C.CONFIG_FILE) assert not os.path.exists(runner.pre_commit_path) - install(runner) + install(runner, store) assert os.path.exists(runner.pre_commit_path) uninstall(runner) assert not os.path.exists(runner.pre_commit_path) @@ -136,29 +136,29 @@ def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): ) -def test_install_pre_commit_and_run(tempdir_factory): +def test_install_pre_commit_and_run(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - assert install(Runner(path, C.CONFIG_FILE)) == 0 + assert install(Runner(path, C.CONFIG_FILE), store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) -def test_install_pre_commit_and_run_custom_path(tempdir_factory): +def test_install_pre_commit_and_run_custom_path(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): cmd_output('git', 'mv', C.CONFIG_FILE, 'custom-config.yaml') cmd_output('git', 'commit', '-m', 'move pre-commit config') - assert install(Runner(path, 'custom-config.yaml')) == 0 + assert install(Runner(path, 'custom-config.yaml'), store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) -def test_install_in_submodule_and_run(tempdir_factory): +def test_install_in_submodule_and_run(tempdir_factory, store): src_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') parent_path = git_dir(tempdir_factory) cmd_output('git', 'submodule', 'add', src_path, 'sub', cwd=parent_path) @@ -166,13 +166,13 @@ def test_install_in_submodule_and_run(tempdir_factory): sub_pth = os.path.join(parent_path, 'sub') with cwd(sub_pth): - assert install(Runner(sub_pth, C.CONFIG_FILE)) == 0 + assert install(Runner(sub_pth, C.CONFIG_FILE), store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) -def test_commit_am(tempdir_factory): +def test_commit_am(tempdir_factory, store): """Regression test for #322.""" path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): @@ -183,16 +183,16 @@ def test_commit_am(tempdir_factory): with io.open('unstaged', 'w') as foo_file: foo_file.write('Oh hai') - assert install(Runner(path, C.CONFIG_FILE)) == 0 + assert install(Runner(path, C.CONFIG_FILE), store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 -def test_unicode_merge_commit_message(tempdir_factory): +def test_unicode_merge_commit_message(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - assert install(Runner(path, C.CONFIG_FILE)) == 0 + assert install(Runner(path, C.CONFIG_FILE), store) == 0 cmd_output('git', 'checkout', 'master', '-b', 'foo') cmd_output('git', 'commit', '--allow-empty', '-n', '-m', 'branch2') cmd_output('git', 'checkout', 'master') @@ -204,11 +204,11 @@ def test_unicode_merge_commit_message(tempdir_factory): ) -def test_install_idempotent(tempdir_factory): +def test_install_idempotent(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - assert install(Runner(path, C.CONFIG_FILE)) == 0 - assert install(Runner(path, C.CONFIG_FILE)) == 0 + assert install(Runner(path, C.CONFIG_FILE), store) == 0 + assert install(Runner(path, C.CONFIG_FILE), store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -223,12 +223,12 @@ def _path_without_us(): ]) -def test_environment_not_sourced(tempdir_factory): +def test_environment_not_sourced(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): # Patch the executable to simulate rming virtualenv with mock.patch.object(sys, 'executable', '/does-not-exist'): - assert install(Runner(path, C.CONFIG_FILE)) == 0 + assert install(Runner(path, C.CONFIG_FILE), store) == 0 # Use a specific homedir to ignore --user installs homedir = tempdir_factory.get() @@ -264,10 +264,10 @@ def test_environment_not_sourced(tempdir_factory): ) -def test_failing_hooks_returns_nonzero(tempdir_factory): +def test_failing_hooks_returns_nonzero(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'failing_hook_repo') with cwd(path): - assert install(Runner(path, C.CONFIG_FILE)) == 0 + assert install(Runner(path, C.CONFIG_FILE), store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 1 @@ -282,7 +282,7 @@ def test_failing_hooks_returns_nonzero(tempdir_factory): ) -def test_install_existing_hooks_no_overwrite(tempdir_factory): +def test_install_existing_hooks_no_overwrite(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path, C.CONFIG_FILE) @@ -299,7 +299,7 @@ def test_install_existing_hooks_no_overwrite(tempdir_factory): assert EXISTING_COMMIT_RUN.match(output) # Now install pre-commit (no-overwrite) - assert install(runner) == 0 + assert install(runner, store) == 0 # We should run both the legacy and pre-commit hooks ret, output = _get_commit_output(tempdir_factory) @@ -308,7 +308,7 @@ def test_install_existing_hooks_no_overwrite(tempdir_factory): assert NORMAL_PRE_COMMIT_RUN.match(output[len('legacy hook\n'):]) -def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory): +def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path, C.CONFIG_FILE) @@ -320,8 +320,8 @@ def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory): make_executable(runner.pre_commit_path) # Install twice - assert install(runner) == 0 - assert install(runner) == 0 + assert install(runner, store) == 0 + assert install(runner, store) == 0 # We should run both the legacy and pre-commit hooks ret, output = _get_commit_output(tempdir_factory) @@ -337,7 +337,7 @@ def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory): ) -def test_failing_existing_hook_returns_1(tempdir_factory): +def test_failing_existing_hook_returns_1(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path, C.CONFIG_FILE) @@ -348,7 +348,7 @@ def test_failing_existing_hook_returns_1(tempdir_factory): hook_file.write('#!/usr/bin/env bash\necho "fail!"\nexit 1\n') make_executable(runner.pre_commit_path) - assert install(runner) == 0 + assert install(runner, store) == 0 # We should get a failure from the legacy hook ret, output = _get_commit_output(tempdir_factory) @@ -356,17 +356,18 @@ def test_failing_existing_hook_returns_1(tempdir_factory): assert FAIL_OLD_HOOK.match(output) -def test_install_overwrite_no_existing_hooks(tempdir_factory): +def test_install_overwrite_no_existing_hooks(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - assert install(Runner(path, C.CONFIG_FILE), overwrite=True) == 0 + runner = Runner(path, C.CONFIG_FILE) + assert install(runner, store, overwrite=True) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) -def test_install_overwrite(tempdir_factory): +def test_install_overwrite(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path, C.CONFIG_FILE) @@ -377,14 +378,14 @@ def test_install_overwrite(tempdir_factory): hook_file.write('#!/usr/bin/env bash\necho "legacy hook"\n') make_executable(runner.pre_commit_path) - assert install(runner, overwrite=True) == 0 + assert install(runner, store, overwrite=True) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) -def test_uninstall_restores_legacy_hooks(tempdir_factory): +def test_uninstall_restores_legacy_hooks(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path, C.CONFIG_FILE) @@ -396,7 +397,7 @@ def test_uninstall_restores_legacy_hooks(tempdir_factory): make_executable(runner.pre_commit_path) # Now install and uninstall pre-commit - assert install(runner) == 0 + assert install(runner, store) == 0 assert uninstall(runner) == 0 # Make sure we installed the "old" hook correctly @@ -405,7 +406,7 @@ def test_uninstall_restores_legacy_hooks(tempdir_factory): assert EXISTING_COMMIT_RUN.match(output) -def test_replace_old_commit_script(tempdir_factory): +def test_replace_old_commit_script(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path, C.CONFIG_FILE) @@ -424,7 +425,7 @@ def test_replace_old_commit_script(tempdir_factory): make_executable(runner.pre_commit_path) # Install normally - assert install(runner) == 0 + assert install(runner, store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -453,39 +454,36 @@ def test_uninstall_doesnt_remove_not_our_hooks(tempdir_factory): ) -def test_installs_hooks_with_hooks_True( - tempdir_factory, - mock_out_store_directory, -): +def test_installs_hooks_with_hooks_True(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - install(Runner(path, C.CONFIG_FILE), hooks=True) + install(Runner(path, C.CONFIG_FILE), store, hooks=True) ret, output = _get_commit_output( - tempdir_factory, pre_commit_home=mock_out_store_directory, + tempdir_factory, pre_commit_home=store.directory, ) assert ret == 0 assert PRE_INSTALLED.match(output) -def test_install_hooks_command(tempdir_factory, mock_out_store_directory): +def test_install_hooks_command(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path, C.CONFIG_FILE) - install(runner) - install_hooks(runner) + install(runner, store) + install_hooks(runner, store) ret, output = _get_commit_output( - tempdir_factory, pre_commit_home=mock_out_store_directory, + tempdir_factory, pre_commit_home=store.directory, ) assert ret == 0 assert PRE_INSTALLED.match(output) -def test_installed_from_venv(tempdir_factory): +def test_installed_from_venv(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - install(Runner(path, C.CONFIG_FILE)) + install(Runner(path, C.CONFIG_FILE), store) # No environment so pre-commit is not on the path when running! # Should still pick up the python from when we installed ret, output = _get_commit_output( @@ -519,12 +517,12 @@ def _get_push_output(tempdir_factory): )[:2] -def test_pre_push_integration_failing(tempdir_factory): +def test_pre_push_integration_failing(tempdir_factory, store): upstream = make_consuming_repo(tempdir_factory, 'failing_hook_repo') path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - install(Runner(path, C.CONFIG_FILE), hook_type='pre-push') + install(Runner(path, C.CONFIG_FILE), store, hook_type='pre-push') # commit succeeds because pre-commit is only installed for pre-push assert _get_commit_output(tempdir_factory)[0] == 0 @@ -535,12 +533,12 @@ def test_pre_push_integration_failing(tempdir_factory): assert 'hookid: failing_hook' in output -def test_pre_push_integration_accepted(tempdir_factory): +def test_pre_push_integration_accepted(tempdir_factory, store): upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo') path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - install(Runner(path, C.CONFIG_FILE), hook_type='pre-push') + install(Runner(path, C.CONFIG_FILE), store, hook_type='pre-push') assert _get_commit_output(tempdir_factory)[0] == 0 retc, output = _get_push_output(tempdir_factory) @@ -549,13 +547,13 @@ def test_pre_push_integration_accepted(tempdir_factory): assert 'Passed' in output -def test_pre_push_new_upstream(tempdir_factory): +def test_pre_push_new_upstream(tempdir_factory, store): upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo') upstream2 = git_dir(tempdir_factory) path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - install(Runner(path, C.CONFIG_FILE), hook_type='pre-push') + install(Runner(path, C.CONFIG_FILE), store, hook_type='pre-push') assert _get_commit_output(tempdir_factory)[0] == 0 cmd_output('git', 'remote', 'rename', 'origin', 'upstream') @@ -566,19 +564,19 @@ def test_pre_push_new_upstream(tempdir_factory): assert 'Passed' in output -def test_pre_push_integration_empty_push(tempdir_factory): +def test_pre_push_integration_empty_push(tempdir_factory, store): upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo') path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - install(Runner(path, C.CONFIG_FILE), hook_type='pre-push') + install(Runner(path, C.CONFIG_FILE), store, hook_type='pre-push') _get_push_output(tempdir_factory) retc, output = _get_push_output(tempdir_factory) assert output == 'Everything up-to-date\n' assert retc == 0 -def test_pre_push_legacy(tempdir_factory): +def test_pre_push_legacy(tempdir_factory, store): upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo') path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) @@ -597,7 +595,7 @@ def test_pre_push_legacy(tempdir_factory): ) make_executable(hook_path) - install(runner, hook_type='pre-push') + install(runner, store, hook_type='pre-push') assert _get_commit_output(tempdir_factory)[0] == 0 retc, output = _get_push_output(tempdir_factory) @@ -608,16 +606,22 @@ def test_pre_push_legacy(tempdir_factory): assert third_line.endswith('Passed') -def test_commit_msg_integration_failing(commit_msg_repo, tempdir_factory): - install(Runner(commit_msg_repo, C.CONFIG_FILE), hook_type='commit-msg') +def test_commit_msg_integration_failing( + commit_msg_repo, tempdir_factory, store, +): + runner = Runner(commit_msg_repo, C.CONFIG_FILE) + install(runner, store, hook_type='commit-msg') retc, out = _get_commit_output(tempdir_factory) assert retc == 1 assert out.startswith('Must have "Signed off by:"...') assert out.strip().endswith('...Failed') -def test_commit_msg_integration_passing(commit_msg_repo, tempdir_factory): - install(Runner(commit_msg_repo, C.CONFIG_FILE), hook_type='commit-msg') +def test_commit_msg_integration_passing( + commit_msg_repo, tempdir_factory, store, +): + runner = Runner(commit_msg_repo, C.CONFIG_FILE) + install(runner, store, hook_type='commit-msg') msg = 'Hi\nSigned off by: me, lol' retc, out = _get_commit_output(tempdir_factory, commit_msg=msg) assert retc == 0 @@ -626,7 +630,7 @@ def test_commit_msg_integration_passing(commit_msg_repo, tempdir_factory): assert first_line.endswith('...Passed') -def test_commit_msg_legacy(commit_msg_repo, tempdir_factory): +def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): runner = Runner(commit_msg_repo, C.CONFIG_FILE) hook_path = runner.get_hook_path('commit-msg') @@ -640,7 +644,7 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory): ) make_executable(hook_path) - install(runner, hook_type='commit-msg') + install(runner, store, hook_type='commit-msg') msg = 'Hi\nSigned off by: asottile' retc, out = _get_commit_output(tempdir_factory, commit_msg=msg) @@ -650,25 +654,31 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory): assert second_line.startswith('Must have "Signed off by:"...') -def test_install_disallow_mising_config(tempdir_factory): +def test_install_disallow_mising_config(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path, C.CONFIG_FILE) remove_config_from_repo(path) - assert install(runner, overwrite=True, skip_on_missing_conf=False) == 0 + ret = install( + runner, store, overwrite=True, skip_on_missing_conf=False, + ) + assert ret == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 1 -def test_install_allow_mising_config(tempdir_factory): +def test_install_allow_mising_config(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path, C.CONFIG_FILE) remove_config_from_repo(path) - assert install(runner, overwrite=True, skip_on_missing_conf=True) == 0 + ret = install( + runner, store, overwrite=True, skip_on_missing_conf=True, + ) + assert ret == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -679,13 +689,16 @@ def test_install_allow_mising_config(tempdir_factory): assert expected in output -def test_install_temporarily_allow_mising_config(tempdir_factory): +def test_install_temporarily_allow_mising_config(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path, C.CONFIG_FILE) remove_config_from_repo(path) - assert install(runner, overwrite=True, skip_on_missing_conf=False) == 0 + ret = install( + runner, store, overwrite=True, skip_on_missing_conf=False, + ) + assert ret == 0 env = dict(os.environ, PRE_COMMIT_ALLOW_NO_CONFIG='1') ret, output = _get_commit_output(tempdir_factory, env=env) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 91e84d98c..70a6b6ec8 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -48,33 +48,32 @@ def stage_a_file(filename='foo.py'): cmd_output('git', 'add', filename) -def _do_run(cap_out, repo, args, environ={}, config_file=C.CONFIG_FILE): +def _do_run(cap_out, store, repo, args, environ={}, config_file=C.CONFIG_FILE): runner = Runner(repo, config_file) with cwd(runner.git_root): # replicates Runner.create behaviour - ret = run(runner, args, environ=environ) + ret = run(runner, store, args, environ=environ) printed = cap_out.get_bytes() return ret, printed def _test_run( - cap_out, repo, opts, expected_outputs, expected_ret, stage, + cap_out, store, repo, opts, expected_outputs, expected_ret, stage, config_file=C.CONFIG_FILE, ): if stage: stage_a_file() args = run_opts(**opts) - ret, printed = _do_run(cap_out, repo, args, config_file=config_file) + ret, printed = _do_run(cap_out, store, repo, args, config_file=config_file) assert ret == expected_ret, (ret, expected_ret, printed) for expected_output_part in expected_outputs: assert expected_output_part in printed -def test_run_all_hooks_failing( - cap_out, repo_with_failing_hook, mock_out_store_directory, -): +def test_run_all_hooks_failing(cap_out, store, repo_with_failing_hook): _test_run( cap_out, + store, repo_with_failing_hook, {}, ( @@ -88,17 +87,15 @@ def test_run_all_hooks_failing( ) -def test_arbitrary_bytes_hook( - cap_out, tempdir_factory, mock_out_store_directory, -): +def test_arbitrary_bytes_hook(cap_out, store, tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'arbitrary_bytes_repo') with cwd(git_path): - _test_run(cap_out, git_path, {}, (b'\xe2\x98\x83\xb2\n',), 1, True) + _test_run( + cap_out, store, git_path, {}, (b'\xe2\x98\x83\xb2\n',), 1, True, + ) -def test_hook_that_modifies_but_returns_zero( - cap_out, tempdir_factory, mock_out_store_directory, -): +def test_hook_that_modifies_but_returns_zero(cap_out, store, tempdir_factory): git_path = make_consuming_repo( tempdir_factory, 'modified_file_returns_zero_repo', ) @@ -106,6 +103,7 @@ def test_hook_that_modifies_but_returns_zero( stage_a_file('bar.py') _test_run( cap_out, + store, git_path, {}, ( @@ -126,22 +124,18 @@ def test_hook_that_modifies_but_returns_zero( ) -def test_types_hook_repository( - cap_out, tempdir_factory, mock_out_store_directory, -): +def test_types_hook_repository(cap_out, store, tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'types_repo') with cwd(git_path): stage_a_file('bar.py') stage_a_file('bar.notpy') - ret, printed = _do_run(cap_out, git_path, run_opts()) + ret, printed = _do_run(cap_out, store, git_path, run_opts()) assert ret == 1 assert b'bar.py' in printed assert b'bar.notpy' not in printed -def test_exclude_types_hook_repository( - cap_out, tempdir_factory, mock_out_store_directory, -): +def test_exclude_types_hook_repository(cap_out, store, tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'exclude_types_repo') with cwd(git_path): with io.open('exe', 'w') as exe: @@ -149,13 +143,13 @@ def test_exclude_types_hook_repository( make_executable('exe') cmd_output('git', 'add', 'exe') stage_a_file('bar.py') - ret, printed = _do_run(cap_out, git_path, run_opts()) + ret, printed = _do_run(cap_out, store, git_path, run_opts()) assert ret == 1 assert b'bar.py' in printed assert b'exe' not in printed -def test_global_exclude(cap_out, tempdir_factory, mock_out_store_directory): +def test_global_exclude(cap_out, store, tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(git_path): with modify_config() as config: @@ -163,23 +157,22 @@ def test_global_exclude(cap_out, tempdir_factory, mock_out_store_directory): open('foo.py', 'a').close() open('bar.py', 'a').close() cmd_output('git', 'add', '.') - ret, printed = _do_run(cap_out, git_path, run_opts(verbose=True)) + opts = run_opts(verbose=True) + ret, printed = _do_run(cap_out, store, git_path, opts) assert ret == 0 # Does not contain foo.py since it was excluded expected = b'hookid: bash_hook\n\nbar.py\nHello World\n\n' assert printed.endswith(expected) -def test_show_diff_on_failure( - capfd, cap_out, tempdir_factory, mock_out_store_directory, -): +def test_show_diff_on_failure(capfd, cap_out, store, tempdir_factory): git_path = make_consuming_repo( tempdir_factory, 'modified_file_returns_zero_repo', ) with cwd(git_path): stage_a_file('bar.py') _test_run( - cap_out, git_path, {'show_diff_on_failure': True}, + cap_out, store, git_path, {'show_diff_on_failure': True}, # we're only testing the output after running (), 1, True, ) @@ -211,15 +204,16 @@ def test_show_diff_on_failure( ) def test_run( cap_out, + store, repo_with_passing_hook, options, outputs, expected_ret, stage, - mock_out_store_directory, ): _test_run( cap_out, + store, repo_with_passing_hook, options, outputs, @@ -228,12 +222,7 @@ def test_run( ) -def test_run_output_logfile( - cap_out, - tempdir_factory, - mock_out_store_directory, -): - +def test_run_output_logfile(cap_out, store, tempdir_factory): expected_output = ( b'This is STDOUT output\n', b'This is STDERR output\n', @@ -243,6 +232,7 @@ def test_run_output_logfile( with cwd(git_path): _test_run( cap_out, + store, git_path, {}, expected_output, expected_ret=1, @@ -257,13 +247,12 @@ def test_run_output_logfile( assert expected_output_part in logfile_content -def test_always_run( - cap_out, repo_with_passing_hook, mock_out_store_directory, -): +def test_always_run(cap_out, store, repo_with_passing_hook): with modify_config() as config: config['repos'][0]['hooks'][0]['always_run'] = True _test_run( cap_out, + store, repo_with_passing_hook, {}, (b'Bash hook', b'Passed'), @@ -272,9 +261,7 @@ def test_always_run( ) -def test_always_run_alt_config( - cap_out, repo_with_passing_hook, mock_out_store_directory, -): +def test_always_run_alt_config(cap_out, store, repo_with_passing_hook): repo_root = '.' config = read_config(repo_root) config['repos'][0]['hooks'][0]['always_run'] = True @@ -283,6 +270,7 @@ def test_always_run_alt_config( _test_run( cap_out, + store, repo_with_passing_hook, {}, (b'Bash hook', b'Passed'), @@ -292,15 +280,14 @@ def test_always_run_alt_config( ) -def test_hook_verbose_enabled( - cap_out, repo_with_passing_hook, mock_out_store_directory, -): +def test_hook_verbose_enabled(cap_out, store, repo_with_passing_hook): with modify_config() as config: config['repos'][0]['hooks'][0]['always_run'] = True config['repos'][0]['hooks'][0]['verbose'] = True _test_run( cap_out, + store, repo_with_passing_hook, {}, (b'Hello World',), @@ -310,26 +297,22 @@ def test_hook_verbose_enabled( @pytest.mark.parametrize( - ('origin', 'source', 'expect_failure'), - ( - ('master', 'master', False), - ('master', '', True), - ('', 'master', True), - ), + ('origin', 'source'), (('master', ''), ('', 'master')), ) -def test_origin_source_error_msg( - repo_with_passing_hook, origin, source, expect_failure, - mock_out_store_directory, cap_out, +def test_origin_source_error_msg_error( + cap_out, store, repo_with_passing_hook, origin, source, ): args = run_opts(origin=origin, source=source) - ret, printed = _do_run(cap_out, repo_with_passing_hook, args) - warning_msg = b'Specify both --origin and --source.' - if expect_failure: - assert ret == 1 - assert warning_msg in printed - else: - assert ret == 0 - assert warning_msg not in printed + ret, printed = _do_run(cap_out, store, repo_with_passing_hook, args) + assert ret == 1 + assert b'Specify both --origin and --source.' in printed + + +def test_origin_source_both_ok(cap_out, store, repo_with_passing_hook): + args = run_opts(origin='master', source='master') + ret, printed = _do_run(cap_out, store, repo_with_passing_hook, args) + assert ret == 0 + assert b'Specify both --origin and --source.' not in printed def test_has_unmerged_paths(in_merge_conflict): @@ -338,30 +321,26 @@ def test_has_unmerged_paths(in_merge_conflict): assert _has_unmerged_paths() is False -def test_merge_conflict(cap_out, in_merge_conflict, mock_out_store_directory): - ret, printed = _do_run(cap_out, in_merge_conflict, run_opts()) +def test_merge_conflict(cap_out, store, in_merge_conflict): + ret, printed = _do_run(cap_out, store, in_merge_conflict, run_opts()) assert ret == 1 assert b'Unmerged files. Resolve before committing.' in printed -def test_merge_conflict_modified( - cap_out, in_merge_conflict, mock_out_store_directory, -): +def test_merge_conflict_modified(cap_out, store, in_merge_conflict): # Touch another file so we have unstaged non-conflicting things assert os.path.exists('dummy') with open('dummy', 'w') as dummy_file: dummy_file.write('bar\nbaz\n') - ret, printed = _do_run(cap_out, in_merge_conflict, run_opts()) + ret, printed = _do_run(cap_out, store, in_merge_conflict, run_opts()) assert ret == 1 assert b'Unmerged files. Resolve before committing.' in printed -def test_merge_conflict_resolved( - cap_out, in_merge_conflict, mock_out_store_directory, -): +def test_merge_conflict_resolved(cap_out, store, in_merge_conflict): cmd_output('git', 'add', '.') - ret, printed = _do_run(cap_out, in_merge_conflict, run_opts()) + ret, printed = _do_run(cap_out, store, in_merge_conflict, run_opts()) for msg in ( b'Checking merge-conflict files only.', b'Bash hook', b'Passed', ): @@ -402,51 +381,45 @@ def test_get_skips(environ, expected_output): assert ret == expected_output -def test_skip_hook(cap_out, repo_with_passing_hook, mock_out_store_directory): +def test_skip_hook(cap_out, store, repo_with_passing_hook): ret, printed = _do_run( - cap_out, repo_with_passing_hook, run_opts(), {'SKIP': 'bash_hook'}, + cap_out, store, repo_with_passing_hook, run_opts(), + {'SKIP': 'bash_hook'}, ) for msg in (b'Bash hook', b'Skipped'): assert msg in printed def test_hook_id_not_in_non_verbose_output( - cap_out, repo_with_passing_hook, mock_out_store_directory, + cap_out, store, repo_with_passing_hook, ): ret, printed = _do_run( - cap_out, repo_with_passing_hook, run_opts(verbose=False), + cap_out, store, repo_with_passing_hook, run_opts(verbose=False), ) assert b'[bash_hook]' not in printed -def test_hook_id_in_verbose_output( - cap_out, repo_with_passing_hook, mock_out_store_directory, -): +def test_hook_id_in_verbose_output(cap_out, store, repo_with_passing_hook): ret, printed = _do_run( - cap_out, repo_with_passing_hook, run_opts(verbose=True), + cap_out, store, repo_with_passing_hook, run_opts(verbose=True), ) assert b'[bash_hook] Bash hook' in printed -def test_multiple_hooks_same_id( - cap_out, repo_with_passing_hook, mock_out_store_directory, -): +def test_multiple_hooks_same_id(cap_out, store, repo_with_passing_hook): with cwd(repo_with_passing_hook): # Add bash hook on there again with modify_config() as config: config['repos'][0]['hooks'].append({'id': 'bash_hook'}) stage_a_file() - ret, output = _do_run(cap_out, repo_with_passing_hook, run_opts()) + ret, output = _do_run(cap_out, store, repo_with_passing_hook, run_opts()) assert ret == 0 assert output.count(b'Bash hook') == 2 -def test_non_ascii_hook_id( - repo_with_passing_hook, mock_out_store_directory, tempdir_factory, -): +def test_non_ascii_hook_id(repo_with_passing_hook, tempdir_factory): with cwd(repo_with_passing_hook): - install(Runner(repo_with_passing_hook, C.CONFIG_FILE)) _, stdout, _ = cmd_output_mocked_pre_commit_home( sys.executable, '-m', 'pre_commit.main', 'run', '☃', retcode=None, tempdir_factory=tempdir_factory, @@ -456,15 +429,13 @@ def test_non_ascii_hook_id( assert 'UnicodeEncodeError' not in stdout -def test_stdout_write_bug_py26( - repo_with_failing_hook, mock_out_store_directory, tempdir_factory, -): +def test_stdout_write_bug_py26(repo_with_failing_hook, store, tempdir_factory): with cwd(repo_with_failing_hook): with modify_config() as config: config['repos'][0]['hooks'][0]['args'] = ['☃'] stage_a_file() - install(Runner(repo_with_failing_hook, C.CONFIG_FILE)) + install(Runner(repo_with_failing_hook, C.CONFIG_FILE), store) # Have to use subprocess because pytest monkeypatches sys.stdout _, stdout, _ = cmd_output_mocked_pre_commit_home( @@ -479,7 +450,7 @@ def test_stdout_write_bug_py26( assert 'UnicodeDecodeError' not in stdout -def test_lots_of_files(mock_out_store_directory, tempdir_factory): +def test_lots_of_files(store, tempdir_factory): # windows xargs seems to have a bug, here's a regression test for # our workaround git_path = make_consuming_repo(tempdir_factory, 'python_hooks_repo') @@ -494,7 +465,7 @@ def test_lots_of_files(mock_out_store_directory, tempdir_factory): open(filename, 'w').close() cmd_output('git', 'add', '.') - install(Runner(git_path, C.CONFIG_FILE)) + install(Runner(git_path, C.CONFIG_FILE), store) cmd_output_mocked_pre_commit_home( 'git', 'commit', '-m', 'Commit!', @@ -504,7 +475,7 @@ def test_lots_of_files(mock_out_store_directory, tempdir_factory): ) -def test_stages(cap_out, repo_with_passing_hook, mock_out_store_directory): +def test_stages(cap_out, store, repo_with_passing_hook): config = OrderedDict(( ('repo', 'local'), ( @@ -526,7 +497,7 @@ def test_stages(cap_out, repo_with_passing_hook, mock_out_store_directory): def _run_for_stage(stage): args = run_opts(hook_stage=stage) - ret, printed = _do_run(cap_out, repo_with_passing_hook, args) + ret, printed = _do_run(cap_out, store, repo_with_passing_hook, args) assert not ret, (ret, printed) # this test should only run one hook assert printed.count(b'hook ') == 1 @@ -537,13 +508,14 @@ def _run_for_stage(stage): assert _run_for_stage('manual').startswith(b'hook 3...') -def test_commit_msg_hook(cap_out, commit_msg_repo, mock_out_store_directory): +def test_commit_msg_hook(cap_out, store, commit_msg_repo): filename = '.git/COMMIT_EDITMSG' with io.open(filename, 'w') as f: f.write('This is the commit message') _test_run( cap_out, + store, commit_msg_repo, {'hook_stage': 'commit-msg', 'commit_msg_filename': filename}, expected_outputs=[b'Must have "Signed off by:"', b'Failed'], @@ -552,9 +524,7 @@ def test_commit_msg_hook(cap_out, commit_msg_repo, mock_out_store_directory): ) -def test_local_hook_passes( - cap_out, repo_with_passing_hook, mock_out_store_directory, -): +def test_local_hook_passes(cap_out, store, repo_with_passing_hook): config = OrderedDict(( ('repo', 'local'), ( @@ -583,6 +553,7 @@ def test_local_hook_passes( _test_run( cap_out, + store, repo_with_passing_hook, opts={}, expected_outputs=[b''], @@ -591,9 +562,7 @@ def test_local_hook_passes( ) -def test_local_hook_fails( - cap_out, repo_with_passing_hook, mock_out_store_directory, -): +def test_local_hook_fails(cap_out, store, repo_with_passing_hook): config = OrderedDict(( ('repo', 'local'), ( @@ -614,6 +583,7 @@ def test_local_hook_fails( _test_run( cap_out, + store, repo_with_passing_hook, opts={}, expected_outputs=[b''], @@ -622,9 +592,7 @@ def test_local_hook_fails( ) -def test_pcre_deprecation_warning( - cap_out, repo_with_passing_hook, mock_out_store_directory, -): +def test_pcre_deprecation_warning(cap_out, store, repo_with_passing_hook): config = OrderedDict(( ('repo', 'local'), ( @@ -640,6 +608,7 @@ def test_pcre_deprecation_warning( _test_run( cap_out, + store, repo_with_passing_hook, opts={}, expected_outputs=[ @@ -651,9 +620,7 @@ def test_pcre_deprecation_warning( ) -def test_meta_hook_passes( - cap_out, repo_with_passing_hook, mock_out_store_directory, -): +def test_meta_hook_passes(cap_out, store, repo_with_passing_hook): config = OrderedDict(( ('repo', 'meta'), ( @@ -668,6 +635,7 @@ def test_meta_hook_passes( _test_run( cap_out, + store, repo_with_passing_hook, opts={}, expected_outputs=[b'Check for useless excludes'], @@ -684,32 +652,25 @@ def modified_config_repo(repo_with_passing_hook): yield repo_with_passing_hook -def test_error_with_unstaged_config( - cap_out, modified_config_repo, mock_out_store_directory, -): +def test_error_with_unstaged_config(cap_out, store, modified_config_repo): args = run_opts() - ret, printed = _do_run(cap_out, modified_config_repo, args) + ret, printed = _do_run(cap_out, store, modified_config_repo, args) assert b'Your pre-commit configuration is unstaged.' in printed assert ret == 1 @pytest.mark.parametrize( - 'opts', ({'all_files': True}, {'files': [C.CONFIG_FILE]}), + 'opts', (run_opts(all_files=True), run_opts(files=[C.CONFIG_FILE])), ) def test_no_unstaged_error_with_all_files_or_files( - cap_out, modified_config_repo, mock_out_store_directory, opts, + cap_out, store, modified_config_repo, opts, ): - args = run_opts(**opts) - ret, printed = _do_run(cap_out, modified_config_repo, args) + ret, printed = _do_run(cap_out, store, modified_config_repo, opts) assert b'Your pre-commit configuration is unstaged.' not in printed -def test_files_running_subdir( - repo_with_passing_hook, mock_out_store_directory, tempdir_factory, -): +def test_files_running_subdir(repo_with_passing_hook, tempdir_factory): with cwd(repo_with_passing_hook): - install(Runner(repo_with_passing_hook, C.CONFIG_FILE)) - os.mkdir('subdir') open('subdir/foo.py', 'w').close() cmd_output('git', 'add', 'subdir/foo.py') @@ -735,35 +696,30 @@ def test_files_running_subdir( ), ) def test_pass_filenames( - cap_out, repo_with_passing_hook, mock_out_store_directory, - pass_filenames, - hook_args, - expected_out, + cap_out, store, repo_with_passing_hook, + pass_filenames, hook_args, expected_out, ): with modify_config() as config: config['repos'][0]['hooks'][0]['pass_filenames'] = pass_filenames config['repos'][0]['hooks'][0]['args'] = hook_args stage_a_file() ret, printed = _do_run( - cap_out, repo_with_passing_hook, run_opts(verbose=True), + cap_out, store, repo_with_passing_hook, run_opts(verbose=True), ) assert expected_out + b'\nHello World' in printed assert (b'foo.py' in printed) == pass_filenames -def test_fail_fast( - cap_out, repo_with_failing_hook, mock_out_store_directory, -): - with cwd(repo_with_failing_hook): - with modify_config() as config: - # More than one hook - config['fail_fast'] = True - config['repos'][0]['hooks'] *= 2 - stage_a_file() +def test_fail_fast(cap_out, store, repo_with_failing_hook): + with modify_config() as config: + # More than one hook + config['fail_fast'] = True + config['repos'][0]['hooks'] *= 2 + stage_a_file() - ret, printed = _do_run(cap_out, repo_with_failing_hook, run_opts()) - # it should have only run one hook - assert printed.count(b'Failing hook') == 1 + ret, printed = _do_run(cap_out, store, repo_with_failing_hook, run_opts()) + # it should have only run one hook + assert printed.count(b'Failing hook') == 1 @pytest.fixture diff --git a/tests/conftest.py b/tests/conftest.py index eb2ecf6e7..f56bb8f45 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -134,7 +134,7 @@ def configure_logging(): @pytest.fixture -def mock_out_store_directory(tempdir_factory): +def mock_store_dir(tempdir_factory): tmpdir = tempdir_factory.get() with mock.patch.object( Store, diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 36eb1faf1..40299b149 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -73,14 +73,14 @@ def test_error_handler_uncaught_error(mocked_log_and_exit): ) -def test_log_and_exit(cap_out, mock_out_store_directory): +def test_log_and_exit(cap_out, mock_store_dir): with pytest.raises(SystemExit): error_handler._log_and_exit( 'msg', error_handler.FatalError('hai'), "I'm a stacktrace", ) printed = cap_out.get() - log_file = os.path.join(mock_out_store_directory, 'pre-commit.log') + log_file = os.path.join(mock_store_dir, 'pre-commit.log') assert printed == ( 'msg: FatalError: hai\n' 'Check the log at {}\n'.format(log_file) @@ -94,7 +94,7 @@ def test_log_and_exit(cap_out, mock_out_store_directory): ) -def test_error_handler_non_ascii_exception(mock_out_store_directory): +def test_error_handler_non_ascii_exception(mock_store_dir): with pytest.raises(SystemExit): with error_handler.error_handler(): raise ValueError('☃') diff --git a/tests/main_test.py b/tests/main_test.py index ae6a73e7f..65adc477a 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -92,13 +92,13 @@ def test_help_other_command( @pytest.mark.parametrize('command', CMDS) -def test_all_cmds(command, mock_commands): +def test_all_cmds(command, mock_commands, mock_store_dir): main.main((command,)) assert getattr(mock_commands, command.replace('-', '_')).call_count == 1 assert_only_one_mock_called(mock_commands) -def test_try_repo(): +def test_try_repo(mock_store_dir): with mock.patch.object(main, 'try_repo') as patch: main.main(('try-repo', '.')) assert patch.call_count == 1 @@ -123,12 +123,12 @@ def test_help_cmd_in_empty_directory( def test_expected_fatal_error_no_git_repo( - tempdir_factory, cap_out, mock_out_store_directory, + tempdir_factory, cap_out, mock_store_dir, ): with cwd(tempdir_factory.get()): with pytest.raises(SystemExit): main.main([]) - log_file = os.path.join(mock_out_store_directory, 'pre-commit.log') + log_file = os.path.join(mock_store_dir, 'pre-commit.log') assert cap_out.get() == ( 'An error has occurred: FatalError: git failed. ' 'Is it installed, and are you in a Git repository directory?\n' @@ -136,6 +136,6 @@ def test_expected_fatal_error_no_git_repo( ) -def test_warning_on_tags_only(mock_commands, cap_out): +def test_warning_on_tags_only(mock_commands, cap_out, mock_store_dir): main.main(('autoupdate', '--tags-only')) assert '--tags-only is the default' in cap_out.get() diff --git a/tests/meta_hooks/check_hooks_apply_test.py b/tests/meta_hooks/check_hooks_apply_test.py index c777daa87..f0f38d695 100644 --- a/tests/meta_hooks/check_hooks_apply_test.py +++ b/tests/meta_hooks/check_hooks_apply_test.py @@ -6,9 +6,7 @@ from testing.util import cwd -def test_hook_excludes_everything( - capsys, tempdir_factory, mock_out_store_directory, -): +def test_hook_excludes_everything(capsys, tempdir_factory, mock_store_dir): config = OrderedDict(( ('repo', 'meta'), ( @@ -31,9 +29,7 @@ def test_hook_excludes_everything( assert 'check-useless-excludes does not apply to this repository' in out -def test_hook_includes_nothing( - capsys, tempdir_factory, mock_out_store_directory, -): +def test_hook_includes_nothing(capsys, tempdir_factory, mock_store_dir): config = OrderedDict(( ('repo', 'meta'), ( @@ -56,9 +52,7 @@ def test_hook_includes_nothing( assert 'check-useless-excludes does not apply to this repository' in out -def test_hook_types_not_matched( - capsys, tempdir_factory, mock_out_store_directory, -): +def test_hook_types_not_matched(capsys, tempdir_factory, mock_store_dir): config = OrderedDict(( ('repo', 'meta'), ( @@ -82,7 +76,7 @@ def test_hook_types_not_matched( def test_hook_types_excludes_everything( - capsys, tempdir_factory, mock_out_store_directory, + capsys, tempdir_factory, mock_store_dir, ): config = OrderedDict(( ('repo', 'meta'), @@ -106,9 +100,7 @@ def test_hook_types_excludes_everything( assert 'check-useless-excludes does not apply to this repository' in out -def test_valid_includes( - capsys, tempdir_factory, mock_out_store_directory, -): +def test_valid_includes(capsys, tempdir_factory, mock_store_dir): config = OrderedDict(( ('repo', 'meta'), ( diff --git a/tests/runner_test.py b/tests/runner_test.py index df3247125..10b1409fb 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -2,14 +2,11 @@ from __future__ import unicode_literals import os.path -from collections import OrderedDict import pre_commit.constants as C from pre_commit.runner import Runner from pre_commit.util import cmd_output -from testing.fixtures import add_config_to_repo from testing.fixtures import git_dir -from testing.fixtures import make_consuming_repo from testing.util import cwd @@ -48,77 +45,6 @@ def test_config_file_path(): assert runner.config_file_path == expected_path -def test_repositories(tempdir_factory, mock_out_store_directory): - path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - runner = Runner(path, C.CONFIG_FILE) - assert len(runner.repositories) == 1 - - -def test_local_hooks(tempdir_factory, mock_out_store_directory): - config = OrderedDict(( - ('repo', 'local'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'arg-per-line'), - ('name', 'Args per line hook'), - ('entry', 'bin/hook.sh'), - ('language', 'script'), - ('files', ''), - ('args', ['hello', 'world']), - )), OrderedDict(( - ('id', 'do_not_commit'), - ('name', 'Block if "DO NOT COMMIT" is found'), - ('entry', 'DO NOT COMMIT'), - ('language', 'pygrep'), - ('files', '^(.*)$'), - )), - ), - ), - )) - git_path = git_dir(tempdir_factory) - add_config_to_repo(git_path, config) - runner = Runner(git_path, C.CONFIG_FILE) - assert len(runner.repositories) == 1 - assert len(runner.repositories[0].hooks) == 2 - - -def test_local_hooks_alt_config(tempdir_factory, mock_out_store_directory): - config = OrderedDict(( - ('repo', 'local'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'arg-per-line'), - ('name', 'Args per line hook'), - ('entry', 'bin/hook.sh'), - ('language', 'script'), - ('files', ''), - ('args', ['hello', 'world']), - )), OrderedDict(( - ('id', 'ugly-format-json'), - ('name', 'Ugly format json'), - ('entry', 'ugly-format-json'), - ('language', 'python'), - ('files', ''), - )), OrderedDict(( - ('id', 'do_not_commit'), - ('name', 'Block if "DO NOT COMMIT" is found'), - ('entry', 'DO NOT COMMIT'), - ('language', 'pygrep'), - ('files', '^(.*)$'), - )), - ), - ), - )) - git_path = git_dir(tempdir_factory) - alt_config_file = 'alternate_config.yaml' - add_config_to_repo(git_path, config, config_file=alt_config_file) - runner = Runner(git_path, alt_config_file) - assert len(runner.repositories) == 1 - assert len(runner.repositories[0].hooks) == 3 - - def test_pre_commit_path(in_tmpdir): path = os.path.join('foo', 'bar') cmd_output('git', 'init', path) From b87c4fd8cc5d6a19e4b8d85f8a76c07c873eef55 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 1 Jul 2018 19:04:07 -0700 Subject: [PATCH 212/544] Remove more properties from Runner --- pre_commit/repository.py | 2 +- pre_commit/runner.py | 16 +---- pre_commit/store.py | 4 +- tests/commands/install_uninstall_test.py | 91 ++++++++++-------------- tests/runner_test.py | 17 ----- 5 files changed, 43 insertions(+), 87 deletions(-) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 0f12bd9e8..e78fba162 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -218,7 +218,7 @@ def _prefix_from_deps(self, language_name, deps): else: return Prefix(self.store.make_local(deps)) - @cached_property + @property def manifest(self): raise NotImplementedError diff --git a/pre_commit/runner.py b/pre_commit/runner.py index a6d0f576a..c172d3fc4 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -27,11 +27,7 @@ def create(cls, config_file): os.chdir(root) return cls(root, config_file) - @cached_property - def git_dir(self): - return git.get_git_dir(self.git_root) - - @cached_property + @property def config_file_path(self): return os.path.join(self.git_root, self.config_file) @@ -40,12 +36,4 @@ def config(self): return load_config(self.config_file_path) def get_hook_path(self, hook_type): - return os.path.join(self.git_dir, 'hooks', hook_type) - - @cached_property - def pre_commit_path(self): - return self.get_hook_path('pre-commit') - - @cached_property - def pre_push_path(self): - return self.get_hook_path('pre-push') + return os.path.join(git.get_git_dir(self.git_root), 'hooks', hook_type) diff --git a/pre_commit/store.py b/pre_commit/store.py index 0ca6b7060..07702fb5d 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -7,8 +7,6 @@ import sqlite3 import tempfile -from cached_property import cached_property - import pre_commit.constants as C from pre_commit import file_lock from pre_commit.util import clean_path_on_failure @@ -173,6 +171,6 @@ def _git_cmd(*args): 'local', C.LOCAL_REPO_VERSION, deps, make_local_strategy, ) - @cached_property + @property def db_path(self): return os.path.join(self.directory, 'db.db') diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 83ea38d32..9f8056916 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -49,21 +49,21 @@ def test_install_pre_commit(tempdir_factory, store): path = git_dir(tempdir_factory) runner = Runner(path, C.CONFIG_FILE) assert not install(runner, store) - assert os.access(runner.pre_commit_path, os.X_OK) + assert os.access(os.path.join(path, '.git/hooks/pre-commit'), os.X_OK) assert not install(runner, store, hook_type='pre-push') - assert os.access(runner.pre_push_path, os.X_OK) + assert os.access(os.path.join(path, '.git/hooks/pre-push'), os.X_OK) def test_install_hooks_directory_not_present(tempdir_factory, store): path = git_dir(tempdir_factory) # Simulate some git clients which don't make .git/hooks #234 - hooks = os.path.join(path, '.git', 'hooks') + hooks = os.path.join(path, '.git/hooks') if os.path.exists(hooks): # pragma: no cover (latest git) shutil.rmtree(hooks) runner = Runner(path, C.CONFIG_FILE) install(runner, store) - assert os.path.exists(runner.pre_commit_path) + assert os.path.exists(os.path.join(path, '.git/hooks/pre-commit')) def test_install_refuses_core_hookspath(tempdir_factory, store): @@ -80,10 +80,10 @@ def test_install_hooks_dead_symlink( ): # pragma: no cover (non-windows) path = git_dir(tempdir_factory) runner = Runner(path, C.CONFIG_FILE) - mkdirp(os.path.dirname(runner.pre_commit_path)) - os.symlink('/fake/baz', os.path.join(path, '.git', 'hooks', 'pre-commit')) + mkdirp(os.path.join(path, '.git/hooks')) + os.symlink('/fake/baz', os.path.join(path, '.git/hooks/pre-commit')) install(runner, store) - assert os.path.exists(runner.pre_commit_path) + assert os.path.exists(os.path.join(path, '.git/hooks/pre-commit')) def test_uninstall_does_not_blow_up_when_not_there(tempdir_factory): @@ -96,11 +96,11 @@ def test_uninstall_does_not_blow_up_when_not_there(tempdir_factory): def test_uninstall(tempdir_factory, store): path = git_dir(tempdir_factory) runner = Runner(path, C.CONFIG_FILE) - assert not os.path.exists(runner.pre_commit_path) + assert not os.path.exists(os.path.join(path, '.git/hooks/pre-commit')) install(runner, store) - assert os.path.exists(runner.pre_commit_path) + assert os.path.exists(os.path.join(path, '.git/hooks/pre-commit')) uninstall(runner) - assert not os.path.exists(runner.pre_commit_path) + assert not os.path.exists(os.path.join(path, '.git/hooks/pre-commit')) def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): @@ -282,16 +282,19 @@ def test_failing_hooks_returns_nonzero(tempdir_factory, store): ) +def _write_legacy_hook(path): + mkdirp(os.path.join(path, '.git/hooks')) + with io.open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: + f.write('#!/usr/bin/env bash\necho "legacy hook"\n') + make_executable(f.name) + + def test_install_existing_hooks_no_overwrite(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path, C.CONFIG_FILE) - # Write out an "old" hook - mkdirp(os.path.dirname(runner.pre_commit_path)) - with io.open(runner.pre_commit_path, 'w') as hook_file: - hook_file.write('#!/usr/bin/env bash\necho "legacy hook"\n') - make_executable(runner.pre_commit_path) + _write_legacy_hook(path) # Make sure we installed the "old" hook correctly ret, output = _get_commit_output(tempdir_factory, touch_file='baz') @@ -313,11 +316,7 @@ def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory, store): with cwd(path): runner = Runner(path, C.CONFIG_FILE) - # Write out an "old" hook - mkdirp(os.path.dirname(runner.pre_commit_path)) - with io.open(runner.pre_commit_path, 'w') as hook_file: - hook_file.write('#!/usr/bin/env bash\necho "legacy hook"\n') - make_executable(runner.pre_commit_path) + _write_legacy_hook(path) # Install twice assert install(runner, store) == 0 @@ -343,10 +342,10 @@ def test_failing_existing_hook_returns_1(tempdir_factory, store): runner = Runner(path, C.CONFIG_FILE) # Write out a failing "old" hook - mkdirp(os.path.dirname(runner.pre_commit_path)) - with io.open(runner.pre_commit_path, 'w') as hook_file: - hook_file.write('#!/usr/bin/env bash\necho "fail!"\nexit 1\n') - make_executable(runner.pre_commit_path) + mkdirp(os.path.join(path, '.git/hooks')) + with io.open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: + f.write('#!/usr/bin/env bash\necho "fail!"\nexit 1\n') + make_executable(f.name) assert install(runner, store) == 0 @@ -372,12 +371,7 @@ def test_install_overwrite(tempdir_factory, store): with cwd(path): runner = Runner(path, C.CONFIG_FILE) - # Write out the "old" hook - mkdirp(os.path.dirname(runner.pre_commit_path)) - with io.open(runner.pre_commit_path, 'w') as hook_file: - hook_file.write('#!/usr/bin/env bash\necho "legacy hook"\n') - make_executable(runner.pre_commit_path) - + _write_legacy_hook(path) assert install(runner, store, overwrite=True) == 0 ret, output = _get_commit_output(tempdir_factory) @@ -390,11 +384,7 @@ def test_uninstall_restores_legacy_hooks(tempdir_factory, store): with cwd(path): runner = Runner(path, C.CONFIG_FILE) - # Write out an "old" hook - mkdirp(os.path.dirname(runner.pre_commit_path)) - with io.open(runner.pre_commit_path, 'w') as hook_file: - hook_file.write('#!/usr/bin/env bash\necho "legacy hook"\n') - make_executable(runner.pre_commit_path) + _write_legacy_hook(path) # Now install and uninstall pre-commit assert install(runner, store) == 0 @@ -412,17 +402,15 @@ def test_replace_old_commit_script(tempdir_factory, store): runner = Runner(path, C.CONFIG_FILE) # Install a script that looks like our old script - pre_commit_contents = io.open( - resource_filename('hook-tmpl'), - ).read() + pre_commit_contents = io.open(resource_filename('hook-tmpl')).read() new_contents = pre_commit_contents.replace( CURRENT_HASH, PRIOR_HASHES[-1], ) - mkdirp(os.path.dirname(runner.pre_commit_path)) - with io.open(runner.pre_commit_path, 'w') as pre_commit_file: - pre_commit_file.write(new_contents) - make_executable(runner.pre_commit_path) + mkdirp(os.path.join(path, '.git/hooks')) + with io.open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: + f.write(new_contents) + make_executable(f.name) # Install normally assert install(runner, store) == 0 @@ -436,14 +424,14 @@ def test_uninstall_doesnt_remove_not_our_hooks(tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): runner = Runner(path, C.CONFIG_FILE) - mkdirp(os.path.dirname(runner.pre_commit_path)) - with io.open(runner.pre_commit_path, 'w') as pre_commit_file: - pre_commit_file.write('#!/usr/bin/env bash\necho 1\n') - make_executable(runner.pre_commit_path) + mkdirp(os.path.join(path, '.git/hooks')) + with io.open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: + f.write('#!/usr/bin/env bash\necho 1\n') + make_executable(f.name) assert uninstall(runner) == 0 - assert os.path.exists(runner.pre_commit_path) + assert os.path.exists(os.path.join(path, '.git/hooks/pre-commit')) PRE_INSTALLED = re.compile( @@ -583,17 +571,16 @@ def test_pre_push_legacy(tempdir_factory, store): with cwd(path): runner = Runner(path, C.CONFIG_FILE) - hook_path = runner.get_hook_path('pre-push') - mkdirp(os.path.dirname(hook_path)) - with io.open(hook_path, 'w') as hook_file: - hook_file.write( + mkdirp(os.path.join(path, '.git/hooks')) + with io.open(os.path.join(path, '.git/hooks/pre-push'), 'w') as f: + f.write( '#!/usr/bin/env bash\n' 'set -eu\n' 'read lr ls rr rs\n' 'test -n "$lr" -a -n "$ls" -a -n "$rr" -a -n "$rs"\n' 'echo legacy\n', ) - make_executable(hook_path) + make_executable(f.name) install(runner, store, hook_type='pre-push') assert _get_commit_output(tempdir_factory)[0] == 0 diff --git a/tests/runner_test.py b/tests/runner_test.py index 10b1409fb..8d1c0421d 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -5,7 +5,6 @@ import pre_commit.constants as C from pre_commit.runner import Runner -from pre_commit.util import cmd_output from testing.fixtures import git_dir from testing.util import cwd @@ -43,19 +42,3 @@ def test_config_file_path(): runner = Runner(os.path.join('foo', 'bar'), C.CONFIG_FILE) expected_path = os.path.join('foo', 'bar', C.CONFIG_FILE) assert runner.config_file_path == expected_path - - -def test_pre_commit_path(in_tmpdir): - path = os.path.join('foo', 'bar') - cmd_output('git', 'init', path) - runner = Runner(path, C.CONFIG_FILE) - expected_path = os.path.join(path, '.git', 'hooks', 'pre-commit') - assert runner.pre_commit_path == expected_path - - -def test_pre_push_path(in_tmpdir): - path = os.path.join('foo', 'bar') - cmd_output('git', 'init', path) - runner = Runner(path, C.CONFIG_FILE) - expected_path = os.path.join(path, '.git', 'hooks', 'pre-push') - assert runner.pre_push_path == expected_path From c294be513d0f00f6a2a0dcab15bb191984a88c40 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 2 Jul 2018 09:59:23 -0700 Subject: [PATCH 213/544] Fix force-push without fetch --- pre_commit/resources/hook-tmpl | 6 +++++- tests/commands/install_uninstall_test.py | 26 +++++++++++++++++++++--- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 2a9657ed2..d3575857e 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -105,6 +105,10 @@ def _exe(): ) +def _rev_exists(rev): + return not subprocess.call(('git', 'rev-list', '--quiet', rev)) + + def _pre_push(stdin): remote = sys.argv[1] @@ -113,7 +117,7 @@ def _pre_push(stdin): _, local_sha, _, remote_sha = line.split() if local_sha == Z40: continue - elif remote_sha != Z40: + elif remote_sha != Z40 and _rev_exists(remote_sha): opts = ('--origin', local_sha, '--source', remote_sha) else: # First ancestor not found in remote diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 9f8056916..6aa9c7fac 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -495,13 +495,13 @@ def test_installed_from_venv(tempdir_factory, store): assert NORMAL_PRE_COMMIT_RUN.match(output) -def _get_push_output(tempdir_factory): +def _get_push_output(tempdir_factory, opts=()): return cmd_output_mocked_pre_commit_home( - 'git', 'push', 'origin', 'HEAD:new_branch', + 'git', 'push', 'origin', 'HEAD:new_branch', *opts, # git push puts pre-commit to stderr stderr=subprocess.STDOUT, tempdir_factory=tempdir_factory, - retcode=None, + retcode=None )[:2] @@ -535,6 +535,26 @@ def test_pre_push_integration_accepted(tempdir_factory, store): assert 'Passed' in output +def test_pre_push_force_push_without_fetch(tempdir_factory, store): + upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + path1 = tempdir_factory.get() + path2 = tempdir_factory.get() + cmd_output('git', 'clone', upstream, path1) + cmd_output('git', 'clone', upstream, path2) + with cwd(path1): + assert _get_commit_output(tempdir_factory)[0] == 0 + assert _get_push_output(tempdir_factory)[0] == 0 + + with cwd(path2): + install(Runner(path2, C.CONFIG_FILE), store, hook_type='pre-push') + assert _get_commit_output(tempdir_factory, commit_msg='force!')[0] == 0 + + retc, output = _get_push_output(tempdir_factory, opts=('--force',)) + assert retc == 0 + assert 'Bash hook' in output + assert 'Passed' in output + + def test_pre_push_new_upstream(tempdir_factory, store): upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo') upstream2 = git_dir(tempdir_factory) From ebb178a7498996c62c477618fcd80ecd83169186 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 2 Jul 2018 11:00:35 -0700 Subject: [PATCH 214/544] v1.10.3 --- CHANGELOG.md | 8 ++++++++ setup.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 932e4b0e6..248bb4363 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +1.10.3 +====== + +### Fixes +- Fix `pre-push` during a force push without a fetch + - #777 issue by @domenkozar. + - #778 PR by @asottile. + 1.10.2 ====== diff --git a/setup.py b/setup.py index 4f0b897fc..b78dafe86 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.10.2', + version='1.10.3', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From eda684661b2edc13a3698079241146a4932184ba Mon Sep 17 00:00:00 2001 From: Daniele Esposti Date: Tue, 3 Jul 2018 00:26:25 +0100 Subject: [PATCH 215/544] Added Rustup cache --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 84fd3f7d9..1d6f59858 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,3 +31,4 @@ cache: directories: - $HOME/.cache/pip - $HOME/.cache/pre-commit + - $HOME/.rustup From c3e438379ae369cd00e19c6ca388af1d15e59aea Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 3 Jul 2018 05:12:36 -0700 Subject: [PATCH 216/544] Appease yaml.load linters _technically_ yaml.load is unsafe, however the contents being loaded here are previously loaded just above using a safe loader so this is not an abitrary code vector. Fixing it nonetheless :) --- pre_commit/commands/migrate_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index 193a002bc..b43367fb9 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -32,7 +32,7 @@ def _migrate_map(contents): # will yield a valid configuration try: trial_contents = header + 'repos:\n' + rest - yaml.load(trial_contents) + ordered_load(trial_contents) contents = trial_contents except yaml.YAMLError: contents = header + 'repos:\n' + _indent(rest) From e6b6abeb9f5e022d44d2e8d3a98755d4d2bbeb1b Mon Sep 17 00:00:00 2001 From: Daniele Esposti Date: Tue, 3 Jul 2018 01:19:58 +0100 Subject: [PATCH 217/544] Added Swift cache --- .travis.yml | 1 + testing/get-swift.sh | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1d6f59858..094274b02 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,3 +32,4 @@ cache: - $HOME/.cache/pip - $HOME/.cache/pre-commit - $HOME/.rustup + - $HOME/.swift diff --git a/testing/get-swift.sh b/testing/get-swift.sh index a45291e23..e4380a351 100755 --- a/testing/get-swift.sh +++ b/testing/get-swift.sh @@ -11,6 +11,6 @@ fi mkdir -p /tmp/swift pushd /tmp/swift - wget "$SWIFT_URL" -O swift.tar.gz - tar -xf swift.tar.gz --strip 1 + wget -N -c "$SWIFT_URL" -O "$HOME"/.swift/swift.tar.gz + tar -xf "$HOME"/.swift/swift.tar.gz --strip 1 popd From 0f600ea0f06d31edb309345a9fdf2a94af45e0c8 Mon Sep 17 00:00:00 2001 From: Daniele Esposti Date: Wed, 4 Jul 2018 08:36:32 +0100 Subject: [PATCH 218/544] Wget timestamping incompatible with -O --- testing/get-swift.sh | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/testing/get-swift.sh b/testing/get-swift.sh index e4380a351..4b04c66db 100755 --- a/testing/get-swift.sh +++ b/testing/get-swift.sh @@ -4,13 +4,18 @@ set -ex . /etc/lsb-release if [ "$DISTRIB_CODENAME" = "trusty" ]; then - SWIFT_URL='https://swift.org/builds/swift-4.0.3-release/ubuntu1404/swift-4.0.3-RELEASE/swift-4.0.3-RELEASE-ubuntu14.04.tar.gz' + SWIFT_TARBALL="swift-4.0.3-RELEASE-ubuntu14.04.tar.gz" + SWIFT_URL="https://swift.org/builds/swift-4.0.3-release/ubuntu1404/swift-4.0.3-RELEASE/$SWIFT_TARBALL" else - SWIFT_URL='https://swift.org/builds/swift-4.0.3-release/ubuntu1604/swift-4.0.3-RELEASE/swift-4.0.3-RELEASE-ubuntu16.04.tar.gz' + SWIFT_TARBALL="swift-4.0.3-RELEASE-ubuntu16.04.tar.gz" + SWIFT_URL="https://swift.org/builds/swift-4.0.3-release/ubuntu1604/swift-4.0.3-RELEASE/$SWIFT_TARBALL" fi +pushd "$HOME"/.swift + wget -N -c "$SWIFT_URL" +popd + mkdir -p /tmp/swift pushd /tmp/swift - wget -N -c "$SWIFT_URL" -O "$HOME"/.swift/swift.tar.gz - tar -xf "$HOME"/.swift/swift.tar.gz --strip 1 + tar -xf "$HOME"/.swift/"$SWIFT_TARBALL" --strip 1 popd From c2e4040756e330284ed21480130a0f5d6f6afeea Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 4 Jul 2018 14:14:29 -0700 Subject: [PATCH 219/544] Improve not found error with script paths (`./exe`) --- pre_commit/parse_shebang.py | 19 ++++++++++++------- tests/parse_shebang_test.py | 24 ++++++++++++++++++++---- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index 33326819e..5a2ba72fc 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -42,16 +42,21 @@ def find_executable(exe, _environ=None): return None -def normexe(orig_exe): - if os.sep not in orig_exe: - exe = find_executable(orig_exe) +def normexe(orig): + def _error(msg): + raise ExecutableNotFoundError('Executable `{}` {}'.format(orig, msg)) + + if os.sep not in orig and (not os.altsep or os.altsep not in orig): + exe = find_executable(orig) if exe is None: - raise ExecutableNotFoundError( - 'Executable `{}` not found'.format(orig_exe), - ) + _error('not found') return exe + elif not os.access(orig, os.X_OK): + _error('not found') + elif os.path.isdir(orig): + _error('is a directory') else: - return orig_exe + return orig def normalize_cmd(cmd): diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index 3f87aea82..bcd6964ba 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -85,6 +85,22 @@ def test_normexe_does_not_exist(): assert excinfo.value.args == ('Executable `i-dont-exist-lol` not found',) +def test_normexe_does_not_exist_sep(): + with pytest.raises(OSError) as excinfo: + parse_shebang.normexe('./i-dont-exist-lol') + assert excinfo.value.args == ('Executable `./i-dont-exist-lol` not found',) + + +def test_normexe_is_a_directory(tmpdir): + with tmpdir.as_cwd(): + tmpdir.join('exe').ensure_dir() + exe = os.path.join('.', 'exe') + with pytest.raises(OSError) as excinfo: + parse_shebang.normexe(exe) + msg, = excinfo.value.args + assert msg == 'Executable `{}` is a directory'.format(exe) + + def test_normexe_already_full_path(): assert parse_shebang.normexe(sys.executable) == sys.executable @@ -107,14 +123,14 @@ def test_normalize_cmd_PATH(): def test_normalize_cmd_shebang(in_tmpdir): - python = distutils.spawn.find_executable('python') - path = write_executable(python.replace(os.sep, '/')) + python = distutils.spawn.find_executable('python').replace(os.sep, '/') + path = write_executable(python) assert parse_shebang.normalize_cmd((path,)) == (python, path) def test_normalize_cmd_PATH_shebang_full_path(in_tmpdir): - python = distutils.spawn.find_executable('python') - path = write_executable(python.replace(os.sep, '/')) + python = distutils.spawn.find_executable('python').replace(os.sep, '/') + path = write_executable(python) with bin_on_path(): ret = parse_shebang.normalize_cmd(('run',)) assert ret == (python, os.path.abspath(path)) From b7ba4a1708cc7f79d01f38ba6f15bd82d977bb72 Mon Sep 17 00:00:00 2001 From: Daniele Esposti Date: Wed, 4 Jul 2018 22:39:07 +0100 Subject: [PATCH 220/544] Added hash of Swift tarballs --- testing/get-swift.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/testing/get-swift.sh b/testing/get-swift.sh index 4b04c66db..54a37e58e 100755 --- a/testing/get-swift.sh +++ b/testing/get-swift.sh @@ -5,14 +5,19 @@ set -ex . /etc/lsb-release if [ "$DISTRIB_CODENAME" = "trusty" ]; then SWIFT_TARBALL="swift-4.0.3-RELEASE-ubuntu14.04.tar.gz" + SWIFT_HASH="dddb40ec4956e4f6a3f4532d859691d5d1ba8822f6e8b4ec6c452172dbede5ae" SWIFT_URL="https://swift.org/builds/swift-4.0.3-release/ubuntu1404/swift-4.0.3-RELEASE/$SWIFT_TARBALL" else SWIFT_TARBALL="swift-4.0.3-RELEASE-ubuntu16.04.tar.gz" + SWIFT_HASH="9adf64cabc7c02ea2d08f150b449b05e46bd42d6e542bf742b3674f5c37f0dbf" SWIFT_URL="https://swift.org/builds/swift-4.0.3-release/ubuntu1604/swift-4.0.3-RELEASE/$SWIFT_TARBALL" fi +mkdir -p "$HOME"/.swift pushd "$HOME"/.swift wget -N -c "$SWIFT_URL" + echo "$SWIFT_HASH $SWIFT_TARBALL" > hash.txt + shasum -a 256 -c hash.txt popd mkdir -p /tmp/swift From c79bc2eb8eedae6c023fd691469c13b89f719ad4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 4 Jul 2018 15:27:23 -0700 Subject: [PATCH 221/544] Oops, this wrote a hash.txt file to the working dir --- testing/get-swift.sh | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/testing/get-swift.sh b/testing/get-swift.sh index 54a37e58e..28986a5f2 100755 --- a/testing/get-swift.sh +++ b/testing/get-swift.sh @@ -1,26 +1,27 @@ #!/usr/bin/env bash # This is a script used in travis-ci to install swift -set -ex +set -euxo pipefail . /etc/lsb-release if [ "$DISTRIB_CODENAME" = "trusty" ]; then - SWIFT_TARBALL="swift-4.0.3-RELEASE-ubuntu14.04.tar.gz" + SWIFT_URL='https://swift.org/builds/swift-4.0.3-release/ubuntu1404/swift-4.0.3-RELEASE/swift-4.0.3-RELEASE-ubuntu14.04.tar.gz' SWIFT_HASH="dddb40ec4956e4f6a3f4532d859691d5d1ba8822f6e8b4ec6c452172dbede5ae" - SWIFT_URL="https://swift.org/builds/swift-4.0.3-release/ubuntu1404/swift-4.0.3-RELEASE/$SWIFT_TARBALL" else - SWIFT_TARBALL="swift-4.0.3-RELEASE-ubuntu16.04.tar.gz" + SWIFT_URL='https://swift.org/builds/swift-4.0.3-release/ubuntu1604/swift-4.0.3-RELEASE/swift-4.0.3-RELEASE-ubuntu16.04.tar.gz' SWIFT_HASH="9adf64cabc7c02ea2d08f150b449b05e46bd42d6e542bf742b3674f5c37f0dbf" - SWIFT_URL="https://swift.org/builds/swift-4.0.3-release/ubuntu1604/swift-4.0.3-RELEASE/$SWIFT_TARBALL" fi -mkdir -p "$HOME"/.swift -pushd "$HOME"/.swift - wget -N -c "$SWIFT_URL" - echo "$SWIFT_HASH $SWIFT_TARBALL" > hash.txt - shasum -a 256 -c hash.txt -popd +check() { + echo "$SWIFT_HASH $TGZ" | sha256sum --check +} + +TGZ="$HOME/.swift/swift.tar.gz" +mkdir -p "$(dirname "$TGZ")" +if ! check >& /dev/null; then + rm -f "$TGZ" + curl --location --silent --output "$TGZ" "$SWIFT_URL" + check +fi mkdir -p /tmp/swift -pushd /tmp/swift - tar -xf "$HOME"/.swift/"$SWIFT_TARBALL" --strip 1 -popd +tar -xf "$TGZ" --strip 1 --directory /tmp/swift From bffa58753d39dacefa05fdbc5411d8ce1c8afafa Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 5 Jul 2018 12:49:01 -0700 Subject: [PATCH 222/544] hook paths are only computed in install_uninstall --- pre_commit/commands/install_uninstall.py | 13 +++++++++---- pre_commit/runner.py | 3 --- tests/commands/install_uninstall_test.py | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 6b2d16f5c..f5947de7b 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -6,6 +6,7 @@ import os.path import sys +from pre_commit import git from pre_commit import output from pre_commit.repository import repositories from pre_commit.util import cmd_output @@ -29,6 +30,11 @@ TEMPLATE_END = '# end templated\n' +def _hook_paths(git_root, hook_type): + pth = os.path.join(git.get_git_dir(git_root), 'hooks', hook_type) + return pth, '{}.legacy'.format(pth) + + def is_our_script(filename): if not os.path.exists(filename): return False @@ -48,8 +54,7 @@ def install( ) return 1 - hook_path = runner.get_hook_path(hook_type) - legacy_path = hook_path + '.legacy' + hook_path, legacy_path = _hook_paths(runner.git_root, hook_type) mkdirp(os.path.dirname(hook_path)) @@ -102,8 +107,8 @@ def install_hooks(runner, store): def uninstall(runner, hook_type='pre-commit'): """Uninstall the pre-commit hooks.""" - hook_path = runner.get_hook_path(hook_type) - legacy_path = hook_path + '.legacy' + hook_path, legacy_path = _hook_paths(runner.git_root, hook_type) + # If our file doesn't exist or it isn't ours, gtfo. if not os.path.exists(hook_path) or not is_our_script(hook_path): return 0 diff --git a/pre_commit/runner.py b/pre_commit/runner.py index c172d3fc4..53107007d 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -34,6 +34,3 @@ def config_file_path(self): @cached_property def config(self): return load_config(self.config_file_path) - - def get_hook_path(self, hook_type): - return os.path.join(git.get_git_dir(self.git_root), 'hooks', hook_type) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 6aa9c7fac..7345cfbd9 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -640,7 +640,7 @@ def test_commit_msg_integration_passing( def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): runner = Runner(commit_msg_repo, C.CONFIG_FILE) - hook_path = runner.get_hook_path('commit-msg') + hook_path = os.path.join(commit_msg_repo, '.git/hooks/commit-msg') mkdirp(os.path.dirname(hook_path)) with io.open(hook_path, 'w') as hook_file: hook_file.write( From ac4aa63ff2888b903e2412fae5998112e427d338 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 5 Jul 2018 13:14:42 -0700 Subject: [PATCH 223/544] Cache rust installation on windows too --- appveyor.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/appveyor.yml b/appveyor.yml index 271edafaf..958acaf99 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -27,3 +27,4 @@ test_script: tox cache: - '%LOCALAPPDATA%\pip\cache' - '%USERPROFILE%\.cache\pre-commit' + - '%USERPROFILE%\.cargo' From 04933cdc46f1d9fb2f3a31a1939122174f3feaa9 Mon Sep 17 00:00:00 2001 From: Daniele Esposti Date: Mon, 2 Jul 2018 21:57:25 +0100 Subject: [PATCH 224/544] Added Python 3.7 to Travis CI --- .travis.yml | 2 ++ setup.py | 2 +- tox.ini | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 094274b02..7a042c858 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,8 @@ matrix: python: 3.6 - env: TOXENV=pypy python: pypy2.7-5.10.0 + - env: TOXENV=py37 + python: 3.7-dev install: pip install coveralls tox script: tox before_install: diff --git a/setup.py b/setup.py index b78dafe86..de1df8232 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ 'cfgv>=1.0.0', 'identify>=1.0.0', 'nodeenv>=0.11.1', - 'pyyaml', + 'pyyaml>=4.2b4', 'six', 'toml', 'virtualenv', diff --git a/tox.ini b/tox.ini index 15674934c..ddc231930 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] project = pre_commit # These should match the travis env list -envlist = py27,py35,py36,pypy +envlist = py27,py35,py36,py37,pypy [testenv] deps = -rrequirements-dev.txt From cec67bb9ad2c3a7b8781bf1322fa03cae3bb4695 Mon Sep 17 00:00:00 2001 From: Daniele Esposti Date: Fri, 6 Jul 2018 08:50:30 +0100 Subject: [PATCH 225/544] Fixed PyYAML version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index de1df8232..86cbf47e6 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ 'cfgv>=1.0.0', 'identify>=1.0.0', 'nodeenv>=0.11.1', - 'pyyaml>=4.2b4', + 'pyyaml>=3.13', 'six', 'toml', 'virtualenv', From 2a46658adca813ec898190ea140cd1d287858cd8 Mon Sep 17 00:00:00 2001 From: Daniele Esposti Date: Fri, 6 Jul 2018 08:50:52 +0100 Subject: [PATCH 226/544] Fixed Travis setup for python 3.7 --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7a042c858..d79dc4a60 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,9 @@ matrix: - env: TOXENV=pypy python: pypy2.7-5.10.0 - env: TOXENV=py37 - python: 3.7-dev + python: 3.7 + sudo: required + dist: xenial install: pip install coveralls tox script: tox before_install: From e4502b73d380e989320714af8da20402ad33c3de Mon Sep 17 00:00:00 2001 From: Daniele Esposti Date: Fri, 6 Jul 2018 19:38:42 +0100 Subject: [PATCH 227/544] Removed PyYAML version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 86cbf47e6..b78dafe86 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ 'cfgv>=1.0.0', 'identify>=1.0.0', 'nodeenv>=0.11.1', - 'pyyaml>=3.13', + 'pyyaml', 'six', 'toml', 'virtualenv', From 707e458984f0db7f11b86bf419d7a9d5e0a0d6cd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 8 Jul 2018 08:19:22 -0700 Subject: [PATCH 228/544] Don't test python3.5 now that we test python3.7 --- .travis.yml | 2 -- setup.py | 2 +- tox.ini | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index d79dc4a60..e59e07179 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,8 +7,6 @@ matrix: include: - env: TOXENV=py27 - env: TOXENV=py27 LATEST_GIT=1 - - env: TOXENV=py35 - python: 3.5 - env: TOXENV=py36 python: 3.6 - env: TOXENV=pypy diff --git a/setup.py b/setup.py index b78dafe86..9c0517e62 100644 --- a/setup.py +++ b/setup.py @@ -21,8 +21,8 @@ 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', ], diff --git a/tox.ini b/tox.ini index ddc231930..b2e657beb 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] project = pre_commit # These should match the travis env list -envlist = py27,py35,py36,py37,pypy +envlist = py27,py36,py37,pypy [testenv] deps = -rrequirements-dev.txt From 49035ba44590c2c767e1effef8fba3ecd84265a5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 8 Jul 2018 14:53:55 -0700 Subject: [PATCH 229/544] tox 3.1 passes PROCESSOR_ARCHITECTURE by default --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index b2e657beb..d4b590bf0 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ envlist = py27,py36,py37,pypy [testenv] deps = -rrequirements-dev.txt -passenv = GOROOT HOME HOMEPATH PROCESSOR_ARCHITECTURE PROGRAMDATA TERM +passenv = GOROOT HOME HOMEPATH PROGRAMDATA TERM commands = coverage erase coverage run -m pytest {posargs:tests} From d2734b2f1b05b3b2a2c2a678b41ce7ba1519e372 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 10 Jul 2018 10:31:42 -0700 Subject: [PATCH 230/544] Use python3.7 in appveyor --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 958acaf99..4b47e5929 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,10 +4,10 @@ environment: TOX_TESTENV_PASSENV: COVERAGE_IGNORE_WINDOWS matrix: - TOXENV: py27 - - TOXENV: py36 + - TOXENV: py37 install: - - "SET PATH=C:\\Python36;C:\\Python36\\Scripts;%PATH%" + - "SET PATH=C:\\Python37;C:\\Python37\\Scripts;%PATH%" - pip install tox virtualenv --upgrade - "mkdir -p C:\\Temp" - "SET TMPDIR=C:\\Temp" From 5b559dbe91f1abc5d2306ba69853c1b15e64c8e3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jul 2018 18:07:14 -0700 Subject: [PATCH 231/544] Temporarily xfail node on windows --- testing/util.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/testing/util.py b/testing/util.py index 6a66c7c9a..43014df4f 100644 --- a/testing/util.py +++ b/testing/util.py @@ -48,16 +48,7 @@ def cmd_output_mocked_pre_commit_home(*args, **kwargs): def broken_deep_listdir(): # pragma: no cover (platform specific) if sys.platform != 'win32': return False - try: - os.listdir(str('\\\\?\\') + os.path.abspath(str('.'))) - except OSError: - return True - try: - os.listdir(b'\\\\?\\C:' + b'\\' * 300) - except TypeError: - return True - except OSError: - return False + return True # see #798 xfailif_broken_deep_listdir = pytest.mark.xfail( From 7d4db5c523abdb9fd0be04f7861d6d401e71b61b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jul 2018 18:23:08 -0700 Subject: [PATCH 232/544] Revert "Merge pull request #788 from pre-commit/cache_cargo_windows" This reverts commit e731aa835ce445cb5ba0cfec8c0637ac6933577c, reversing changes made to a4b5a9f7fb2cb26d8d0c23620b701130f651d8bf. --- appveyor.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 4b47e5929..23d3931c6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -27,4 +27,3 @@ test_script: tox cache: - '%LOCALAPPDATA%\pip\cache' - '%USERPROFILE%\.cache\pre-commit' - - '%USERPROFILE%\.cargo' From c947a0935d143b01c2d91243be660789eb655626 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jul 2018 16:40:57 -0700 Subject: [PATCH 233/544] Fix buffering in --show-diff-on-failure --- pre_commit/commands/run.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index b5dcc1e28..b1549d413 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -1,4 +1,3 @@ -from __future__ import print_function from __future__ import unicode_literals import logging @@ -205,7 +204,7 @@ def _run_hooks(config, repo_hooks, args, environ): args.show_diff_on_failure and subprocess.call(('git', 'diff', '--quiet', '--no-ext-diff')) != 0 ): - print('All changes made by hooks:') + output.write_line('All changes made by hooks:') subprocess.call(('git', '--no-pager', 'diff', '--no-ext-diff')) return retval From 0eaacd7c8e10f96c830a19fa86c165ca7fa72d9f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jul 2018 16:48:09 -0700 Subject: [PATCH 234/544] Default to python3 when using python_venv under python 2 --- pre_commit/languages/python_venv.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/python_venv.py b/pre_commit/languages/python_venv.py index 4397ce183..b7658f5d8 100644 --- a/pre_commit/languages/python_venv.py +++ b/pre_commit/languages/python_venv.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import os.path +import sys from pre_commit.languages import python from pre_commit.util import CalledProcessError @@ -10,6 +11,13 @@ ENVIRONMENT_DIR = 'py_venv' +def get_default_version(): # pragma: no cover (version specific) + if sys.version_info < (3,): + return 'python3' + else: + return python.get_default_version() + + def orig_py_exe(exe): # pragma: no cover (platform specific) """A -mvenv virtualenv made from a -mvirtualenv virtualenv installs packages to the incorrect location. Attempt to find the _original_ exe @@ -43,6 +51,5 @@ def make_venv(envdir, python): cmd_output(orig_py_exe(python), '-mvenv', envdir, cwd='/') -get_default_version = python.get_default_version _interface = python.py_interface(ENVIRONMENT_DIR, make_venv) in_env, healthy, run_hook, install_environment = _interface From 4f419fdaabc34544d77197d89e6d1eed078d39e2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 18 Jul 2018 15:25:48 -0700 Subject: [PATCH 235/544] Revert "Merge pull request #799 from pre-commit/temporarily_skip_npm_windows" This reverts commit 063014ffd833ea8ac6a8fa47e6c95582d6ff2247, reversing changes made to 259ef9e53041b43e746da5b2b1fe5ca9020d70fe. --- testing/util.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/testing/util.py b/testing/util.py index 43014df4f..6a66c7c9a 100644 --- a/testing/util.py +++ b/testing/util.py @@ -48,7 +48,16 @@ def cmd_output_mocked_pre_commit_home(*args, **kwargs): def broken_deep_listdir(): # pragma: no cover (platform specific) if sys.platform != 'win32': return False - return True # see #798 + try: + os.listdir(str('\\\\?\\') + os.path.abspath(str('.'))) + except OSError: + return True + try: + os.listdir(b'\\\\?\\C:' + b'\\' * 300) + except TypeError: + return True + except OSError: + return False xfailif_broken_deep_listdir = pytest.mark.xfail( From 4640dc7b4a39f74c43637510ef56ef330095b813 Mon Sep 17 00:00:00 2001 From: Matt Layman Date: Thu, 19 Jul 2018 21:45:43 -0400 Subject: [PATCH 236/544] Run only the specified hook even when stages exist in config. This branches fixes the run logic so that when `pre-commit run some_hook -a` runs when the config contains `stages: ['commit']` for some other hook, only the hook specified as an argument will run. Fixes #772 --- pre_commit/commands/run.py | 12 +++++++----- tests/commands/run_test.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index b1549d413..f2fb5962d 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -254,11 +254,13 @@ def run(runner, store, args, environ=os.environ): repo_hooks = [] for repo in repositories(runner.config, store): for _, hook in repo.hooks: - if ( - (not args.hook or hook['id'] == args.hook) and - not hook['stages'] or args.hook_stage in hook['stages'] - ): - repo_hooks.append((repo, hook)) + if args.hook: + if args.hook == hook['id']: + repo_hooks.append((repo, hook)) + break + else: + if not hook['stages'] or args.hook_stage in hook['stages']: + repo_hooks.append((repo, hook)) if args.hook and not repo_hooks: output.write_line('No hook with id `{}`'.format(args.hook)) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 70a6b6ec8..ed16ed47d 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -762,3 +762,34 @@ def test_include_exclude_does_search_instead_of_match(some_filenames): def test_include_exclude_exclude_removes_files(some_filenames): ret = _filter_by_include_exclude(some_filenames, '', r'\.py$') assert ret == ['.pre-commit-hooks.yaml'] + + +def test_args_hook_only(cap_out, store, repo_with_passing_hook): + config = OrderedDict(( + ('repo', 'local'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'flake8'), + ('name', 'flake8'), + ('entry', "'{}' -m flake8".format(sys.executable)), + ('language', 'system'), + ('stages', ['commit']), + )), OrderedDict(( + ('id', 'do_not_commit'), + ('name', 'Block if "DO NOT COMMIT" is found'), + ('entry', 'DO NOT COMMIT'), + ('language', 'pygrep'), + )), + ), + ), + )) + add_config_to_repo(repo_with_passing_hook, config) + stage_a_file() + ret, printed = _do_run( + cap_out, + store, + repo_with_passing_hook, + run_opts(hook='do_not_commit'), + ) + assert 'flake8' not in printed From a8b298799c27d52eed2b500182b68b266e99caa5 Mon Sep 17 00:00:00 2001 From: Matt Layman Date: Thu, 19 Jul 2018 22:11:15 -0400 Subject: [PATCH 237/544] Check bytes for Python 3. --- tests/commands/run_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index ed16ed47d..e6258d31b 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -792,4 +792,4 @@ def test_args_hook_only(cap_out, store, repo_with_passing_hook): repo_with_passing_hook, run_opts(hook='do_not_commit'), ) - assert 'flake8' not in printed + assert b'flake8' not in printed From fd1bc21d8e8a7aabd569e7deccba92eb3475e33b Mon Sep 17 00:00:00 2001 From: Matt Layman Date: Thu, 19 Jul 2018 23:27:29 -0400 Subject: [PATCH 238/544] Use parens instead of different logic pattern. --- pre_commit/commands/run.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index f2fb5962d..dbf564102 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -254,13 +254,11 @@ def run(runner, store, args, environ=os.environ): repo_hooks = [] for repo in repositories(runner.config, store): for _, hook in repo.hooks: - if args.hook: - if args.hook == hook['id']: - repo_hooks.append((repo, hook)) - break - else: - if not hook['stages'] or args.hook_stage in hook['stages']: - repo_hooks.append((repo, hook)) + if ( + (not args.hook or hook['id'] == args.hook) and + (not hook['stages'] or args.hook_stage in hook['stages']) + ): + repo_hooks.append((repo, hook)) if args.hook and not repo_hooks: output.write_line('No hook with id `{}`'.format(args.hook)) From 52f39fee12149a1e0102bc8c4d664930239774b2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 22 Jul 2018 08:47:43 -0700 Subject: [PATCH 239/544] v1.10.4 --- CHANGELOG.md | 19 +++++++++++++++++++ setup.py | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 248bb4363..141bdca37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +1.10.4 +====== + +### Fixes +- Replace `yaml.load` with safe alternative + - `yaml.load` can lead to arbitrary code execution, though not where it + was used + - issue by @tonybaloney + - #779 PR by @asottile. +- Improve not found error with script paths (`./exe`) + - #782 issue by @ssbarnea. + - #785 PR by @asottile. + +### Misc +- Improve travis-ci build times by caching rust / swift artifacts + - #781 PR by @expobrain. +- Test against python3.7 + - #789 PR by @expobrain. + 1.10.3 ====== diff --git a/setup.py b/setup.py index 9c0517e62..93464bdb4 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.10.3', + version='1.10.4', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From a3847d830c309737c29843f0e77aff529d88fbd9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 22 Jul 2018 09:25:34 -0700 Subject: [PATCH 240/544] A few changelog entries didn't get commited --- CHANGELOG.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 141bdca37..7824f1269 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,19 @@ - Replace `yaml.load` with safe alternative - `yaml.load` can lead to arbitrary code execution, though not where it was used - - issue by @tonybaloney + - issue by @tonybaloney. - #779 PR by @asottile. - Improve not found error with script paths (`./exe`) - #782 issue by @ssbarnea. - #785 PR by @asottile. +- Fix minor buffering issue during `--show-diff-on-failure` + - #796 PR by @asottile. +- Default `language_version: python3` for `python_venv` when running in python2 + - #794 issue by @ssbarnea. + - #797 PR by @asottile. +- `pre-commit run X` only run `X` and not hooks with `stages: [...]` + - #772 issue by @asottile. + - #803 PR by @mblayman. ### Misc - Improve travis-ci build times by caching rust / swift artifacts From 7e69d117c6d67aa6ac522a7570999457a9708be4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 24 Jul 2018 16:05:26 -0700 Subject: [PATCH 241/544] Work around sys.executable issue using brew python on macos https://github.com/Homebrew/homebrew-core/issues/30445 --- pre_commit/resources/hook-tmpl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index d3575857e..cb25ec50e 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -7,6 +7,9 @@ import os import subprocess import sys +# work around https://github.com/Homebrew/homebrew-core/issues/30445 +os.environ.pop('__PYVENV_LAUNCHER__', None) + HERE = os.path.dirname(os.path.abspath(__file__)) Z40 = '0' * 40 ID_HASH = '138fd403232d2ddd5efb44317e38bf03' From 3f784877695da4b86e76086136dfc58149779f32 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 6 Aug 2018 09:26:27 -0700 Subject: [PATCH 242/544] Support `pre-commit install` inside a worktree --- pre_commit/git.py | 14 ++++++++++---- tests/commands/install_uninstall_test.py | 13 +++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 4fb2e65a1..d9e01f5fc 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -31,10 +31,16 @@ def get_root(): def get_git_dir(git_root): - return os.path.normpath(os.path.join( - git_root, - cmd_output('git', 'rev-parse', '--git-dir', cwd=git_root)[1].strip(), - )) + def _git_dir(opt): + return os.path.normpath(os.path.join( + git_root, + cmd_output('git', 'rev-parse', opt, cwd=git_root)[1].strip(), + )) + + try: + return _git_dir('--git-common-dir') + except CalledProcessError: # pragma: no cover (git < 2.5) + return _git_dir('--git-dir') def get_remote_url(git_root): diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 7345cfbd9..e6f0e417f 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -172,6 +172,19 @@ def test_install_in_submodule_and_run(tempdir_factory, store): assert NORMAL_PRE_COMMIT_RUN.match(output) +def test_install_in_worktree_and_run(tempdir_factory, store): + src_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + path = tempdir_factory.get() + cmd_output('git', '-C', src_path, 'branch', '-m', 'notmaster') + cmd_output('git', '-C', src_path, 'worktree', 'add', path, '-b', 'master') + + with cwd(path): + assert install(Runner(path, C.CONFIG_FILE), store) == 0 + ret, output = _get_commit_output(tempdir_factory) + assert ret == 0 + assert NORMAL_PRE_COMMIT_RUN.match(output) + + def test_commit_am(tempdir_factory, store): """Regression test for #322.""" path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') From ff73f6f741baeae6d6a08da19c9b2f309ddebd38 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 6 Aug 2018 14:03:01 -0700 Subject: [PATCH 243/544] v1.10.5 --- CHANGELOG.md | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7824f1269..c1882766b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +1.10.5 +====== + +### Fixes +- Work around `PATH` issue with `brew` `python` on `macos` + - Homebrew/homebrew-core#30445 issue by @asottile. + - #805 PR by @asottile. +- Support `pre-commit install` inside a worktree + - #808 issue by @s0undt3ch. + - #809 PR by @asottile. + 1.10.4 ====== diff --git a/setup.py b/setup.py index 93464bdb4..c7fd5f65b 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.10.4', + version='1.10.5', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From abee146199f3d558089443a2655880304249d191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Fri, 10 Aug 2018 17:50:15 +0200 Subject: [PATCH 244/544] Get rid of @pytest.mark.integration --- tests/make_archives_test.py | 3 --- tests/repository_test.py | 36 ------------------------------------ 2 files changed, 39 deletions(-) diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py index 60ecb7ac8..7f1983222 100644 --- a/tests/make_archives_test.py +++ b/tests/make_archives_test.py @@ -4,8 +4,6 @@ import os.path import tarfile -import pytest - from pre_commit import git from pre_commit import make_archives from pre_commit.util import cmd_output @@ -47,7 +45,6 @@ def test_make_archive(tempdir_factory): assert not os.path.exists(os.path.join(extract_dir, 'foo', 'bar')) -@pytest.mark.integration def test_main(tmpdir): make_archives.main(('--dest', tmpdir.strpath)) diff --git a/tests/repository_test.py b/tests/repository_test.py index 2ca399ce6..95506eeb6 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -63,7 +63,6 @@ def _test_hook_repo( assert _norm_out(ret[1]) == expected -@pytest.mark.integration def test_python_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'python_hooks_repo', @@ -72,7 +71,6 @@ def test_python_hook(tempdir_factory, store): ) -@pytest.mark.integration def test_python_hook_default_version(tempdir_factory, store): # make sure that this continues to work for platforms where default # language detection does not work @@ -82,7 +80,6 @@ def test_python_hook_default_version(tempdir_factory, store): test_python_hook(tempdir_factory, store) -@pytest.mark.integration def test_python_hook_args_with_spaces(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'python_hooks_repo', @@ -99,7 +96,6 @@ def test_python_hook_args_with_spaces(tempdir_factory, store): ) -@pytest.mark.integration def test_python_hook_weird_setup_cfg(tempdir_factory, store): path = git_dir(tempdir_factory) with cwd(path): @@ -122,7 +118,6 @@ def test_python_venv(tempdir_factory, store): # pragma: no cover (no venv) ) -@pytest.mark.integration def test_switch_language_versions_doesnt_clobber(tempdir_factory, store): # We're using the python3 repo because it prints the python version path = make_repo(tempdir_factory, 'python3_hooks_repo') @@ -145,7 +140,6 @@ def run_on_version(version, expected_output): run_on_version('python3', b'3\n[]\nHello World\n') -@pytest.mark.integration def test_versioned_python_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'python3_hooks_repo', @@ -156,7 +150,6 @@ def test_versioned_python_hook(tempdir_factory, store): @skipif_cant_run_docker -@pytest.mark.integration def test_run_a_docker_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'docker_hooks_repo', @@ -166,7 +159,6 @@ def test_run_a_docker_hook(tempdir_factory, store): @skipif_cant_run_docker -@pytest.mark.integration def test_run_a_docker_hook_with_entry_args(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'docker_hooks_repo', @@ -176,7 +168,6 @@ def test_run_a_docker_hook_with_entry_args(tempdir_factory, store): @skipif_cant_run_docker -@pytest.mark.integration def test_run_a_failing_docker_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'docker_hooks_repo', @@ -187,7 +178,6 @@ def test_run_a_failing_docker_hook(tempdir_factory, store): @skipif_cant_run_docker -@pytest.mark.integration @pytest.mark.parametrize('hook_id', ('echo-entrypoint', 'echo-cmd')) def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): _test_hook_repo( @@ -198,7 +188,6 @@ def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): @xfailif_broken_deep_listdir -@pytest.mark.integration def test_run_a_node_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'node_hooks_repo', @@ -207,7 +196,6 @@ def test_run_a_node_hook(tempdir_factory, store): @xfailif_broken_deep_listdir -@pytest.mark.integration def test_run_versioned_node_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'node_versioned_hooks_repo', @@ -216,7 +204,6 @@ def test_run_versioned_node_hook(tempdir_factory, store): @xfailif_windows_no_ruby -@pytest.mark.integration def test_run_a_ruby_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'ruby_hooks_repo', @@ -225,7 +212,6 @@ def test_run_a_ruby_hook(tempdir_factory, store): @xfailif_windows_no_ruby -@pytest.mark.integration def test_run_versioned_ruby_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'ruby_versioned_hooks_repo', @@ -236,7 +222,6 @@ def test_run_versioned_ruby_hook(tempdir_factory, store): @xfailif_windows_no_ruby -@pytest.mark.integration def test_run_ruby_hook_with_disable_shared_gems( tempdir_factory, store, @@ -258,7 +243,6 @@ def test_run_ruby_hook_with_disable_shared_gems( ) -@pytest.mark.integration def test_system_hook_with_spaces(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'system_hook_with_spaces_repo', @@ -267,7 +251,6 @@ def test_system_hook_with_spaces(tempdir_factory, store): @skipif_cant_run_swift -@pytest.mark.integration def test_swift_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'swift_hooks_repo', @@ -275,7 +258,6 @@ def test_swift_hook(tempdir_factory, store): ) -@pytest.mark.integration def test_golang_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'golang_hooks_repo', @@ -283,7 +265,6 @@ def test_golang_hook(tempdir_factory, store): ) -@pytest.mark.integration def test_rust_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'rust_hooks_repo', @@ -291,7 +272,6 @@ def test_rust_hook(tempdir_factory, store): ) -@pytest.mark.integration @pytest.mark.parametrize('dep', ('cli:shellharden:3.1.0', 'cli:shellharden')) def test_additional_rust_cli_dependencies_installed( tempdir_factory, store, dep, @@ -311,7 +291,6 @@ def test_additional_rust_cli_dependencies_installed( assert 'shellharden' in binaries -@pytest.mark.integration def test_additional_rust_lib_dependencies_installed( tempdir_factory, store, ): @@ -332,7 +311,6 @@ def test_additional_rust_lib_dependencies_installed( assert 'shellharden' not in binaries -@pytest.mark.integration def test_missing_executable(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'not_found_exe', @@ -342,7 +320,6 @@ def test_missing_executable(tempdir_factory, store): ) -@pytest.mark.integration def test_run_a_script_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'script_hooks_repo', @@ -350,7 +327,6 @@ def test_run_a_script_hook(tempdir_factory, store): ) -@pytest.mark.integration def test_run_hook_with_spaced_args(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'arg_per_line_hooks_repo', @@ -360,7 +336,6 @@ def test_run_hook_with_spaced_args(tempdir_factory, store): ) -@pytest.mark.integration def test_run_hook_with_curly_braced_arguments(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'arg_per_line_hooks_repo', @@ -469,7 +444,6 @@ def _norm_pwd(path): )[1].strip() -@pytest.mark.integration def test_cwd_of_hook(tempdir_factory, store): # Note: this doubles as a test for `system` hooks path = git_dir(tempdir_factory) @@ -480,7 +454,6 @@ def test_cwd_of_hook(tempdir_factory, store): ) -@pytest.mark.integration def test_lots_of_files(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'script_hooks_repo', @@ -488,7 +461,6 @@ def test_lots_of_files(tempdir_factory, store): ) -@pytest.mark.integration def test_venvs(tempdir_factory, store): path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) @@ -497,7 +469,6 @@ def test_venvs(tempdir_factory, store): assert venv == (mock.ANY, 'python', python.get_default_version(), []) -@pytest.mark.integration def test_additional_dependencies(tempdir_factory, store): path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) @@ -507,7 +478,6 @@ def test_additional_dependencies(tempdir_factory, store): assert venv == (mock.ANY, 'python', python.get_default_version(), ['pep8']) -@pytest.mark.integration def test_additional_dependencies_roll_forward(tempdir_factory, store): path = make_repo(tempdir_factory, 'python_hooks_repo') @@ -533,7 +503,6 @@ def test_additional_dependencies_roll_forward(tempdir_factory, store): @xfailif_windows_no_ruby -@pytest.mark.integration def test_additional_ruby_dependencies_installed( tempdir_factory, store, ): # pragma: no cover (non-windows) @@ -550,7 +519,6 @@ def test_additional_ruby_dependencies_installed( @xfailif_broken_deep_listdir -@pytest.mark.integration def test_additional_node_dependencies_installed( tempdir_factory, store, ): # pragma: no cover (non-windows) @@ -566,7 +534,6 @@ def test_additional_node_dependencies_installed( assert 'lodash' in output -@pytest.mark.integration def test_additional_golang_dependencies_installed( tempdir_factory, store, ): @@ -695,7 +662,6 @@ def test_invalidated_virtualenv(tempdir_factory, store): assert retv == 0 -@pytest.mark.integration def test_really_long_file_paths(tempdir_factory, store): base_path = tempdir_factory.get() really_long_path = os.path.join(base_path, 'really_long' * 10) @@ -709,7 +675,6 @@ def test_really_long_file_paths(tempdir_factory, store): repo.require_installed() -@pytest.mark.integration def test_config_overrides_repo_specifics(tempdir_factory, store): path = make_repo(tempdir_factory, 'script_hooks_repo') config = make_config_from_repo(path) @@ -729,7 +694,6 @@ def _create_repo_with_tags(tempdir_factory, src, tag): return path -@pytest.mark.integration def test_tags_on_repositories(in_tmpdir, tempdir_factory, store): tag = 'v1.1' git_dir_1 = _create_repo_with_tags(tempdir_factory, 'prints_cwd_repo', tag) From 67d6fcb0f68f2b6737c8430995112d29f09ef4de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Fri, 10 Aug 2018 10:21:20 +0200 Subject: [PATCH 245/544] Fix several ResourceWarning: unclosed file --- pre_commit/commands/autoupdate.py | 3 +- pre_commit/commands/install_uninstall.py | 3 +- pre_commit/git.py | 3 +- pre_commit/repository.py | 3 +- testing/fixtures.py | 9 +++-- tests/commands/autoupdate_test.py | 48 ++++++++++++++++-------- tests/commands/install_uninstall_test.py | 3 +- tests/conftest.py | 20 ++++++++++ tests/error_handler_test.py | 10 ++--- tests/staged_files_only_test.py | 10 ++--- 10 files changed, 78 insertions(+), 34 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 241126ddf..8f3714c49 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -72,7 +72,8 @@ def _update_repo(repo_config, store, tags_only): def _write_new_config_file(path, output): - original_contents = open(path).read() + with open(path) as f: + original_contents = f.read() output = remove_defaults(output, CONFIG_SCHEMA) new_contents = ordered_dump(output, **C.YAML_DUMP_KWARGS) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index f5947de7b..d76a6c1a4 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -38,7 +38,8 @@ def _hook_paths(git_root, hook_type): def is_our_script(filename): if not os.path.exists(filename): return False - contents = io.open(filename).read() + with io.open(filename) as f: + contents = f.read() return any(h in contents for h in (CURRENT_HASH,) + PRIOR_HASHES) diff --git a/pre_commit/git.py b/pre_commit/git.py index d9e01f5fc..9ec9c9fbc 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -70,7 +70,8 @@ def get_conflicted_files(): logger.info('Checking merge-conflict files only.') # Need to get the conflicted files from the MERGE_MSG because they could # have resolved the conflict by choosing one side or the other - merge_msg = open(os.path.join(get_git_dir('.'), 'MERGE_MSG'), 'rb').read() + with open(os.path.join(get_git_dir('.'), 'MERGE_MSG'), 'rb') as f: + merge_msg = f.read() merge_conflict_filenames = parse_merge_msg_for_conflicts(merge_msg) # This will get the rest of the changes made after the merge. diff --git a/pre_commit/repository.py b/pre_commit/repository.py index e78fba162..278f31a2b 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -43,7 +43,8 @@ def _read_state(prefix, venv): if not os.path.exists(filename): return None else: - return json.loads(io.open(filename).read()) + with io.open(filename) as f: + return json.loads(f.read()) def _write_state(prefix, venv, state): diff --git a/testing/fixtures.py b/testing/fixtures.py index fd5c7b43c..cbcb7bb05 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -40,7 +40,8 @@ def modify_manifest(path): .pre-commit-hooks.yaml. """ manifest_path = os.path.join(path, C.MANIFEST_FILE) - manifest = ordered_load(io.open(manifest_path).read()) + with io.open(manifest_path) as f: + manifest = ordered_load(f.read()) yield manifest with io.open(manifest_path, 'w') as manifest_file: manifest_file.write(ordered_dump(manifest, **C.YAML_DUMP_KWARGS)) @@ -55,7 +56,8 @@ def modify_config(path='.', commit=True): .pre-commit-config.yaml """ config_path = os.path.join(path, C.CONFIG_FILE) - config = ordered_load(io.open(config_path).read()) + with io.open(config_path) as f: + config = ordered_load(f.read()) yield config with io.open(config_path, 'w', encoding='UTF-8') as config_file: config_file.write(ordered_dump(config, **C.YAML_DUMP_KWARGS)) @@ -100,7 +102,8 @@ def make_config_from_repo(repo_path, rev=None, hooks=None, check=True): def read_config(directory, config_file=C.CONFIG_FILE): config_path = os.path.join(directory, config_file) - config = ordered_load(io.open(config_path).read()) + with io.open(config_path) as f: + config = ordered_load(f.read()) return config diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 5408d45ae..3bfb62e0e 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -42,10 +42,12 @@ def test_autoupdate_up_to_date_repo(up_to_date_repo, in_tmpdir, store): config = make_config_from_repo(up_to_date_repo, check=False) write_config('.', config) - before = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + before = f.read() assert '^$' not in before ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) - after = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + after = f.read() assert ret == 0 assert before == after @@ -68,9 +70,11 @@ def test_autoupdate_old_revision_broken(tempdir_factory, in_tmpdir, store): config['rev'] = rev write_config('.', config) - before = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + before = f.read() ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) - after = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + after = f.read() assert ret == 0 assert before != after assert update_rev in after @@ -106,9 +110,11 @@ def test_autoupdate_out_of_date_repo(out_of_date_repo, in_tmpdir, store): ) write_config('.', config) - before = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + before = f.read() ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) - after = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + after = f.read() assert ret == 0 assert before != after # Make sure we don't add defaults @@ -128,10 +134,12 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( write_config('.', config) runner = Runner('.', C.CONFIG_FILE) - before = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + before = f.read() repo_name = 'file://{}'.format(out_of_date_repo.path) ret = autoupdate(runner, store, tags_only=False, repos=(repo_name,)) - after = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + after = f.read() assert ret == 0 assert before != after assert out_of_date_repo.head_rev in after @@ -148,10 +156,12 @@ def test_autoupdate_out_of_date_repo_with_wrong_repo_name( write_config('.', config) runner = Runner('.', C.CONFIG_FILE) - before = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + before = f.read() # It will not update it, because the name doesn't match ret = autoupdate(runner, store, tags_only=False, repos=('dne',)) - after = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + after = f.read() assert ret == 0 assert before == after @@ -171,7 +181,8 @@ def test_does_not_reformat(in_tmpdir, out_of_date_repo, store): f.write(config) autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) - after = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + after = f.read() expected = fmt.format(out_of_date_repo.path, out_of_date_repo.head_rev) assert after == expected @@ -200,7 +211,8 @@ def test_loses_formatting_when_not_detectable( f.write(config) autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) - after = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + after = f.read() expected = ( 'repos:\n' '- repo: {}\n' @@ -225,7 +237,8 @@ def test_autoupdate_tagged_repo(tagged_repo, in_tmpdir, store): ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) assert ret == 0 - assert 'v1.2.3' in open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + assert 'v1.2.3' in f.read() @pytest.fixture @@ -243,7 +256,8 @@ def test_autoupdate_tags_only(tagged_repo_with_more_commits, in_tmpdir, store): ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=True) assert ret == 0 - assert 'v1.2.3' in open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + assert 'v1.2.3' in f.read() @pytest.fixture @@ -282,9 +296,11 @@ def test_autoupdate_hook_disappearing_repo( ) write_config('.', config) - before = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + before = f.read() ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) - after = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + after = f.read() assert ret == 1 assert before == after diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index e6f0e417f..40d9beead 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -415,7 +415,8 @@ def test_replace_old_commit_script(tempdir_factory, store): runner = Runner(path, C.CONFIG_FILE) # Install a script that looks like our old script - pre_commit_contents = io.open(resource_filename('hook-tmpl')).read() + with io.open(resource_filename('hook-tmpl')) as f: + pre_commit_contents = f.read() new_contents = pre_commit_contents.replace( CURRENT_HASH, PRIOR_HASHES[-1], ) diff --git a/tests/conftest.py b/tests/conftest.py index f56bb8f45..82daccd4d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,6 +21,26 @@ from testing.util import cwd +@pytest.fixture(autouse=True) +def no_warnings(recwarn): + yield + warnings = [] + for warning in recwarn: # pragma: no cover + message = str(warning.message) + # ImportWarning: Not importing directory '...' missing __init__(.py) + if not ( + isinstance(warning.message, ImportWarning) + and message.startswith('Not importing directory ') + and ' missing __init__' in message + ): + warnings.append('{}:{} {}'.format( + warning.filename, + warning.lineno, + message, + )) + assert not warnings + + @pytest.fixture def tempdir_factory(tmpdir): class TmpdirFactory(object): diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 40299b149..6aebe5a3b 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -87,11 +87,11 @@ def test_log_and_exit(cap_out, mock_store_dir): ) assert os.path.exists(log_file) - contents = io.open(log_file).read() - assert contents == ( - 'msg: FatalError: hai\n' - "I'm a stacktrace\n" - ) + with io.open(log_file) as f: + assert f.read() == ( + 'msg: FatalError: hai\n' + "I'm a stacktrace\n" + ) def test_error_handler_non_ascii_exception(mock_store_dir): diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index b2af9fedb..f5c146688 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -48,7 +48,8 @@ def _test_foo_state( encoding='UTF-8', ): assert os.path.exists(path.foo_filename) - assert io.open(path.foo_filename, encoding=encoding).read() == foo_contents + with io.open(path.foo_filename, encoding=encoding) as f: + assert f.read() == foo_contents actual_status = get_short_git_status()['foo'] assert status == actual_status @@ -144,10 +145,9 @@ def img_staged(tempdir_factory): def _test_img_state(path, expected_file='img1.jpg', status='A'): assert os.path.exists(path.img_filename) - assert ( - io.open(path.img_filename, 'rb').read() == - io.open(get_resource_path(expected_file), 'rb').read() - ) + with io.open(path.img_filename, 'rb') as f1,\ + io.open(get_resource_path(expected_file), 'rb') as f2: + assert f1.read() == f2.read() actual_status = get_short_git_status()['img.jpg'] assert status == actual_status From d68a778e3badc362c16d7a9196ec3948d535e87b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Fri, 10 Aug 2018 17:10:14 +0200 Subject: [PATCH 246/544] Fix the use of deprecated inspect.getargspec() on Python 3 --- tests/languages/all_test.py | 40 +++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index 6e3ab6622..46bc85b12 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -1,20 +1,34 @@ from __future__ import unicode_literals +import functools import inspect import pytest +import six from pre_commit.languages.all import all_languages from pre_commit.languages.all import languages +if six.PY2: # pragma: no cover + ArgSpec = functools.partial( + inspect.ArgSpec, varargs=None, keywords=None, defaults=None, + ) + getargspec = inspect.getargspec +else: + ArgSpec = functools.partial( + inspect.FullArgSpec, varargs=None, varkw=None, defaults=None, + kwonlyargs=[], kwonlydefaults=None, annotations={}, + ) + getargspec = inspect.getfullargspec + + @pytest.mark.parametrize('language', all_languages) def test_install_environment_argspec(language): - expected_argspec = inspect.ArgSpec( + expected_argspec = ArgSpec( args=['prefix', 'version', 'additional_dependencies'], - varargs=None, keywords=None, defaults=None, ) - argspec = inspect.getargspec(languages[language].install_environment) + argspec = getargspec(languages[language].install_environment) assert argspec == expected_argspec @@ -25,28 +39,20 @@ def test_ENVIRONMENT_DIR(language): @pytest.mark.parametrize('language', all_languages) def test_run_hook_argpsec(language): - expected_argspec = inspect.ArgSpec( - args=['prefix', 'hook', 'file_args'], - varargs=None, keywords=None, defaults=None, - ) - argspec = inspect.getargspec(languages[language].run_hook) + expected_argspec = ArgSpec(args=['prefix', 'hook', 'file_args']) + argspec = getargspec(languages[language].run_hook) assert argspec == expected_argspec @pytest.mark.parametrize('language', all_languages) def test_get_default_version_argspec(language): - expected_argspec = inspect.ArgSpec( - args=[], varargs=None, keywords=None, defaults=None, - ) - argspec = inspect.getargspec(languages[language].get_default_version) + expected_argspec = ArgSpec(args=[]) + argspec = getargspec(languages[language].get_default_version) assert argspec == expected_argspec @pytest.mark.parametrize('language', all_languages) def test_healthy_argspec(language): - expected_argspec = inspect.ArgSpec( - args=['prefix', 'language_version'], - varargs=None, keywords=None, defaults=None, - ) - argspec = inspect.getargspec(languages[language].healthy) + expected_argspec = ArgSpec(args=['prefix', 'language_version']) + argspec = getargspec(languages[language].healthy) assert argspec == expected_argspec From a8640c759d54be85b6c538e7c9f62fd47b847c60 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 10 Aug 2018 11:11:50 -0700 Subject: [PATCH 247/544] Add `# pragma: no cover` for the py3-only branch --- tests/languages/all_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index 46bc85b12..3d5d88c76 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -15,7 +15,7 @@ inspect.ArgSpec, varargs=None, keywords=None, defaults=None, ) getargspec = inspect.getargspec -else: +else: # pragma: no cover ArgSpec = functools.partial( inspect.FullArgSpec, varargs=None, varkw=None, defaults=None, kwonlyargs=[], kwonlydefaults=None, annotations={}, From 174d3bf057a56820ea7bc72496a7921389942cb3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 10 Aug 2018 12:33:21 -0700 Subject: [PATCH 248/544] Minor style adjustment --- tests/staged_files_only_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index f5c146688..9e1a0a4c1 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -145,9 +145,9 @@ def img_staged(tempdir_factory): def _test_img_state(path, expected_file='img1.jpg', status='A'): assert os.path.exists(path.img_filename) - with io.open(path.img_filename, 'rb') as f1,\ - io.open(get_resource_path(expected_file), 'rb') as f2: - assert f1.read() == f2.read() + with io.open(path.img_filename, 'rb') as f1 + with io.open(get_resource_path(expected_file), 'rb') as f2: + assert f1.read() == f2.read() actual_status = get_short_git_status()['img.jpg'] assert status == actual_status From cf691e85c89dbe16dce7e0a729649b2e19d4d9ad Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 10 Aug 2018 12:39:40 -0700 Subject: [PATCH 249/544] that's what I get for not waiting for CI --- tests/staged_files_only_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 9e1a0a4c1..42f7ecae5 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -145,7 +145,7 @@ def img_staged(tempdir_factory): def _test_img_state(path, expected_file='img1.jpg', status='A'): assert os.path.exists(path.img_filename) - with io.open(path.img_filename, 'rb') as f1 + with io.open(path.img_filename, 'rb') as f1: with io.open(get_resource_path(expected_file), 'rb') as f2: assert f1.read() == f2.read() actual_status = get_short_git_status()['img.jpg'] From a6e2e1d4bb4fdb773214b004ed72b941d79ec87c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 10 Aug 2018 18:11:28 -0700 Subject: [PATCH 250/544] Add language: fail --- pre_commit/languages/all.py | 2 ++ pre_commit/languages/fail.py | 15 +++++++++++++++ tests/repository_test.py | 23 +++++++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 pre_commit/languages/fail.py diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index be74ffd3a..a019ddffc 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -2,6 +2,7 @@ from pre_commit.languages import docker from pre_commit.languages import docker_image +from pre_commit.languages import fail from pre_commit.languages import golang from pre_commit.languages import node from pre_commit.languages import pcre @@ -54,6 +55,7 @@ languages = { 'docker': docker, 'docker_image': docker_image, + 'fail': fail, 'golang': golang, 'node': node, 'pcre': pcre, diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py new file mode 100644 index 000000000..c69fcae0d --- /dev/null +++ b/pre_commit/languages/fail.py @@ -0,0 +1,15 @@ +from __future__ import unicode_literals + +from pre_commit.languages import helpers + + +ENVIRONMENT_DIR = None +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy +install_environment = helpers.no_install + + +def run_hook(prefix, hook, file_args): + out = hook['entry'].encode('UTF-8') + b'\n\n' + out += b'\n'.join(f.encode('UTF-8') for f in file_args) + b'\n' + return 1, out, b'' diff --git a/tests/repository_test.py b/tests/repository_test.py index 95506eeb6..4c76f9a07 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -589,6 +589,29 @@ def test_local_rust_additional_dependencies(store): assert _norm_out(ret[1]) == b"Hello World!\n" +def test_fail_hooks(store): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'fail', + 'name': 'fail', + 'language': 'fail', + 'entry': 'make sure to name changelogs as .rst!', + 'files': r'changelog/.*(? Date: Wed, 15 Aug 2018 17:55:06 -0700 Subject: [PATCH 251/544] Update config --- .pre-commit-config.yaml | 6 +++--- pre_commit/commands/sample_config.py | 2 +- tests/commands/sample_config_test.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a146bd25b..b5a9260f2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v1.2.3 + rev: v1.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -13,11 +13,11 @@ repos: - id: requirements-txt-fixer - id: flake8 - repo: https://github.com/pre-commit/pre-commit - rev: v1.7.0 + rev: v1.10.5 hooks: - id: validate_manifest - repo: https://github.com/asottile/reorder_python_imports - rev: v1.0.1 + rev: v1.1.0 hooks: - id: reorder-python-imports language_version: python2.7 diff --git a/pre_commit/commands/sample_config.py b/pre_commit/commands/sample_config.py index aef0107e8..87bcaa7d9 100644 --- a/pre_commit/commands/sample_config.py +++ b/pre_commit/commands/sample_config.py @@ -12,7 +12,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v1.2.1-1 + rev: v1.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/tests/commands/sample_config_test.py b/tests/commands/sample_config_test.py index 7c4e88d88..cd43d45f2 100644 --- a/tests/commands/sample_config_test.py +++ b/tests/commands/sample_config_test.py @@ -13,7 +13,7 @@ def test_sample_config(capsys): # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v1.2.1-1 + rev: v1.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer From 1bd6fce7dce2032fa7083c720650e65b80bf5256 Mon Sep 17 00:00:00 2001 From: Jeffrey Rackauckas Date: Wed, 29 Aug 2018 18:54:55 -0700 Subject: [PATCH 252/544] Don't print bogus characters on windows terminals that don't support colors. --- pre_commit/color.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pre_commit/color.py b/pre_commit/color.py index 44917ca04..e75ffd64a 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -3,11 +3,13 @@ import os import sys +terminal_supports_colors = True if os.name == 'nt': # pragma: no cover (windows) from pre_commit.color_windows import enable_virtual_terminal_processing try: enable_virtual_terminal_processing() except WindowsError: + terminal_supports_colors = False pass RED = '\033[41m' @@ -29,7 +31,7 @@ def format_color(text, color, use_color_setting): color - The color start string use_color_setting - Whether or not to color """ - if not use_color_setting: + if not use_color_setting or not terminal_supports_colors: return text else: return '{}{}{}'.format(color, text, NORMAL) From a970d3b69b693fffd858fce7791c0d0168e584ff Mon Sep 17 00:00:00 2001 From: Jeffrey Rackauckas Date: Thu, 30 Aug 2018 18:45:29 -0700 Subject: [PATCH 253/544] Removing useless pass statement. --- pre_commit/color.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pre_commit/color.py b/pre_commit/color.py index e75ffd64a..e3b034208 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -10,7 +10,6 @@ enable_virtual_terminal_processing() except WindowsError: terminal_supports_colors = False - pass RED = '\033[41m' GREEN = '\033[42m' From 3d777bb386fef50a077c1814bcb1a5f26ed1a964 Mon Sep 17 00:00:00 2001 From: Jeffrey Rackauckas Date: Thu, 30 Aug 2018 19:15:46 -0700 Subject: [PATCH 254/544] Move logic to handle terminal not supporting colors to use_color --- pre_commit/color.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pre_commit/color.py b/pre_commit/color.py index e3b034208..c785e2c9f 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -3,13 +3,13 @@ import os import sys -terminal_supports_colors = True +terminal_supports_color = True if os.name == 'nt': # pragma: no cover (windows) from pre_commit.color_windows import enable_virtual_terminal_processing try: enable_virtual_terminal_processing() except WindowsError: - terminal_supports_colors = False + terminal_supports_color = False RED = '\033[41m' GREEN = '\033[42m' @@ -30,7 +30,7 @@ def format_color(text, color, use_color_setting): color - The color start string use_color_setting - Whether or not to color """ - if not use_color_setting or not terminal_supports_colors: + if not use_color_setting: return text else: return '{}{}{}'.format(color, text, NORMAL) @@ -48,4 +48,7 @@ def use_color(setting): if setting not in COLOR_CHOICES: raise InvalidColorSetting(setting) - return setting == 'always' or (setting == 'auto' and sys.stdout.isatty()) + return ( + setting == 'always' or + (setting == 'auto' and sys.stdout.isatty() and terminal_supports_color) + ) From 710eef317ab1bce800636f1e2578a8b3133ca959 Mon Sep 17 00:00:00 2001 From: Jeffrey Rackauckas Date: Thu, 30 Aug 2018 19:39:37 -0700 Subject: [PATCH 255/544] Fixing tests to account for the new terminal_supports_color variable --- tests/color_test.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/color_test.py b/tests/color_test.py index 0b8a4d699..6e11765ce 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -35,9 +35,16 @@ def test_use_color_no_tty(): assert use_color('auto') is False -def test_use_color_tty(): +def test_use_color_tty_with_color_support(): with mock.patch.object(sys.stdout, 'isatty', return_value=True): - assert use_color('auto') is True + with mock.patch('pre_commit.color.terminal_supports_color', True): + assert use_color('auto') is True + + +def test_use_color_tty_without_color_support(): + with mock.patch.object(sys.stdout, 'isatty', return_value=True): + with mock.patch('pre_commit.color.terminal_supports_color', False): + assert use_color('auto') is False def test_use_color_raises_if_given_shenanigans(): From 9d48766c02fec71b6bf7a81e7d84526ae7cd304b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 2 Sep 2018 18:39:11 -0700 Subject: [PATCH 256/544] git mv tests/meta_hooks/{,check_}useless_excludes_test.py --- .../{useless_excludes_test.py => check_useless_excludes_test.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/meta_hooks/{useless_excludes_test.py => check_useless_excludes_test.py} (100%) diff --git a/tests/meta_hooks/useless_excludes_test.py b/tests/meta_hooks/check_useless_excludes_test.py similarity index 100% rename from tests/meta_hooks/useless_excludes_test.py rename to tests/meta_hooks/check_useless_excludes_test.py From 21c2c9df3366e8f2a7544405f9d19af0a0423857 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 2 Sep 2018 18:45:21 -0700 Subject: [PATCH 257/544] No need for OrderedDict --- tests/meta_hooks/check_hooks_apply_test.py | 129 +++++++++--------- .../meta_hooks/check_useless_excludes_test.py | 95 ++++++------- 2 files changed, 109 insertions(+), 115 deletions(-) diff --git a/tests/meta_hooks/check_hooks_apply_test.py b/tests/meta_hooks/check_hooks_apply_test.py index f0f38d695..e6a7b133c 100644 --- a/tests/meta_hooks/check_hooks_apply_test.py +++ b/tests/meta_hooks/check_hooks_apply_test.py @@ -1,5 +1,3 @@ -from collections import OrderedDict - from pre_commit.meta_hooks import check_hooks_apply from testing.fixtures import add_config_to_repo from testing.fixtures import git_dir @@ -7,17 +5,19 @@ def test_hook_excludes_everything(capsys, tempdir_factory, mock_store_dir): - config = OrderedDict(( - ('repo', 'meta'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'check-useless-excludes'), - ('exclude', '.pre-commit-config.yaml'), - )), - ), - ), - )) + config = { + 'repos': [ + { + 'repo': 'meta', + 'hooks': [ + { + 'id': 'check-useless-excludes', + 'exclude': '.pre-commit-config.yaml', + }, + ], + }, + ], + } repo = git_dir(tempdir_factory) add_config_to_repo(repo, config) @@ -30,17 +30,19 @@ def test_hook_excludes_everything(capsys, tempdir_factory, mock_store_dir): def test_hook_includes_nothing(capsys, tempdir_factory, mock_store_dir): - config = OrderedDict(( - ('repo', 'meta'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'check-useless-excludes'), - ('files', 'foo'), - )), - ), - ), - )) + config = { + 'repos': [ + { + 'repo': 'meta', + 'hooks': [ + { + 'id': 'check-useless-excludes', + 'files': 'foo', + }, + ], + }, + ], + } repo = git_dir(tempdir_factory) add_config_to_repo(repo, config) @@ -53,17 +55,19 @@ def test_hook_includes_nothing(capsys, tempdir_factory, mock_store_dir): def test_hook_types_not_matched(capsys, tempdir_factory, mock_store_dir): - config = OrderedDict(( - ('repo', 'meta'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'check-useless-excludes'), - ('types', ['python']), - )), - ), - ), - )) + config = { + 'repos': [ + { + 'repo': 'meta', + 'hooks': [ + { + 'id': 'check-useless-excludes', + 'types': ['python'], + }, + ], + }, + ], + } repo = git_dir(tempdir_factory) add_config_to_repo(repo, config) @@ -78,17 +82,19 @@ def test_hook_types_not_matched(capsys, tempdir_factory, mock_store_dir): def test_hook_types_excludes_everything( capsys, tempdir_factory, mock_store_dir, ): - config = OrderedDict(( - ('repo', 'meta'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'check-useless-excludes'), - ('exclude_types', ['yaml']), - )), - ), - ), - )) + config = { + 'repos': [ + { + 'repo': 'meta', + 'hooks': [ + { + 'id': 'check-useless-excludes', + 'exclude_types': ['yaml'], + }, + ], + }, + ], + } repo = git_dir(tempdir_factory) add_config_to_repo(repo, config) @@ -101,22 +107,21 @@ def test_hook_types_excludes_everything( def test_valid_includes(capsys, tempdir_factory, mock_store_dir): - config = OrderedDict(( - ('repo', 'meta'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'check-useless-excludes'), - )), - # Should not be reported as an error due to always_run - OrderedDict(( - ('id', 'check-useless-excludes'), - ('files', '^$'), - ('always_run', True), - )), - ), - ), - )) + config = { + 'repos': [ + { + 'repo': 'meta', + 'hooks': [ + # Should not be reported as an error due to always_run + { + 'id': 'check-useless-excludes', + 'files': '^$', + 'always_run': True, + }, + ], + }, + ], + } repo = git_dir(tempdir_factory) add_config_to_repo(repo, config) diff --git a/tests/meta_hooks/check_useless_excludes_test.py b/tests/meta_hooks/check_useless_excludes_test.py index 137c357f3..1a03fb085 100644 --- a/tests/meta_hooks/check_useless_excludes_test.py +++ b/tests/meta_hooks/check_useless_excludes_test.py @@ -1,5 +1,3 @@ -from collections import OrderedDict - from pre_commit.meta_hooks import check_useless_excludes from testing.fixtures import add_config_to_repo from testing.fixtures import git_dir @@ -7,23 +5,15 @@ def test_useless_exclude_global(capsys, tempdir_factory): - config = OrderedDict(( - ('exclude', 'foo'), - ( - 'repos', [ - OrderedDict(( - ('repo', 'meta'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'check-useless-excludes'), - )), - ), - ), - )), - ], - ), - )) + config = { + 'exclude': 'foo', + 'repos': [ + { + 'repo': 'meta', + 'hooks': [{'id': 'check-useless-excludes'}], + }, + ], + } repo = git_dir(tempdir_factory) add_config_to_repo(repo, config) @@ -32,21 +22,19 @@ def test_useless_exclude_global(capsys, tempdir_factory): assert check_useless_excludes.main(()) == 1 out, _ = capsys.readouterr() - assert "The global exclude pattern 'foo' does not match any files" in out + out = out.strip() + assert "The global exclude pattern 'foo' does not match any files" == out def test_useless_exclude_for_hook(capsys, tempdir_factory): - config = OrderedDict(( - ('repo', 'meta'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'check-useless-excludes'), - ('exclude', 'foo'), - )), - ), - ), - )) + config = { + 'repos': [ + { + 'repo': 'meta', + 'hooks': [{'id': 'check-useless-excludes', 'exclude': 'foo'}], + }, + ], + } repo = git_dir(tempdir_factory) add_config_to_repo(repo, config) @@ -55,24 +43,23 @@ def test_useless_exclude_for_hook(capsys, tempdir_factory): assert check_useless_excludes.main(()) == 1 out, _ = capsys.readouterr() + out = out.strip() expected = ( "The exclude pattern 'foo' for check-useless-excludes " "does not match any files" ) - assert expected in out + assert expected == out def test_no_excludes(capsys, tempdir_factory): - config = OrderedDict(( - ('repo', 'meta'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'check-useless-excludes'), - )), - ), - ), - )) + config = { + 'repos': [ + { + 'repo': 'meta', + 'hooks': [{'id': 'check-useless-excludes'}], + }, + ], + } repo = git_dir(tempdir_factory) add_config_to_repo(repo, config) @@ -85,17 +72,19 @@ def test_no_excludes(capsys, tempdir_factory): def test_valid_exclude(capsys, tempdir_factory): - config = OrderedDict(( - ('repo', 'meta'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'check-useless-excludes'), - ('exclude', '.pre-commit-config.yaml'), - )), - ), - ), - )) + config = { + 'repos': [ + { + 'repo': 'meta', + 'hooks': [ + { + 'id': 'check-useless-excludes', + 'exclude': '.pre-commit-config.yaml', + }, + ], + }, + ], + } repo = git_dir(tempdir_factory) add_config_to_repo(repo, config) From ce25b652b91966b0dfb528299f8f1f84a3893192 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 2 Sep 2018 18:54:34 -0700 Subject: [PATCH 258/544] Exempt `language: fail` hooks from check-hooks-apply --- pre_commit/meta_hooks/check_hooks_apply.py | 2 +- tests/meta_hooks/check_hooks_apply_test.py | 31 +++++++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index 23420f468..4c4719c8a 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -15,7 +15,7 @@ def check_all_hooks_match_files(config_file): for repo in repositories(load_config(config_file), Store()): for hook_id, hook in repo.hooks: - if hook['always_run']: + if hook['always_run'] or hook['language'] == 'fail': continue include, exclude = hook['files'], hook['exclude'] filtered = _filter_by_include_exclude(files, include, exclude) diff --git a/tests/meta_hooks/check_hooks_apply_test.py b/tests/meta_hooks/check_hooks_apply_test.py index e6a7b133c..c75b036a3 100644 --- a/tests/meta_hooks/check_hooks_apply_test.py +++ b/tests/meta_hooks/check_hooks_apply_test.py @@ -106,7 +106,7 @@ def test_hook_types_excludes_everything( assert 'check-useless-excludes does not apply to this repository' in out -def test_valid_includes(capsys, tempdir_factory, mock_store_dir): +def test_valid_always_run(capsys, tempdir_factory, mock_store_dir): config = { 'repos': [ { @@ -131,3 +131,32 @@ def test_valid_includes(capsys, tempdir_factory, mock_store_dir): out, _ = capsys.readouterr() assert out == '' + + +def test_valid_language_fail(capsys, tempdir_factory, mock_store_dir): + config = { + 'repos': [ + { + 'repo': 'local', + 'hooks': [ + # Should not be reported as an error due to language: fail + { + 'id': 'changelogs-rst', + 'name': 'changelogs must be rst', + 'entry': 'changelog filenames must end in .rst', + 'language': 'fail', + 'files': r'changelog/.*(? Date: Sun, 2 Sep 2018 19:57:09 -0700 Subject: [PATCH 259/544] v1.11.0 --- CHANGELOG.md | 14 ++++++++++++++ setup.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1882766b..3f073ff16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +1.11.0 +====== + +### Features +- Add new `fail` language which always fails + - light-weight way to forbid files by name. + - #812 #821 PRs by @asottile. + +### Fixes +- Fix `ResourceWarning`s for unclosed files + - #811 PR by @BoboTiG. +- Don't write ANSI colors on windows when color enabling fails + - #819 PR by @jeffreyrack. + 1.10.5 ====== diff --git a/setup.py b/setup.py index c7fd5f65b..2c1390544 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.10.5', + version='1.11.0', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From 18b6f4b519e1520c846d4899db15a5ab9b3d9043 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 22 Sep 2018 08:53:52 -0700 Subject: [PATCH 260/544] Fix rev-parse for older git versions --- pre_commit/git.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 9ec9c9fbc..a92611632 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -31,16 +31,13 @@ def get_root(): def get_git_dir(git_root): - def _git_dir(opt): - return os.path.normpath(os.path.join( - git_root, - cmd_output('git', 'rev-parse', opt, cwd=git_root)[1].strip(), - )) - - try: - return _git_dir('--git-common-dir') - except CalledProcessError: # pragma: no cover (git < 2.5) - return _git_dir('--git-dir') + opts = ('--git-common-dir', '--git-dir') + _, out, _ = cmd_output('git', 'rev-parse', *opts, cwd=git_root) + for line, opt in zip(out.splitlines(), opts): + if line != opt: # pragma: no branch (git < 2.5) + return os.path.normpath(os.path.join(git_root, line)) + else: + raise AssertionError('unreachable: no git dir') def get_remote_url(git_root): From 08319101f4e0cd0f1dfa53ac353111329e260ffb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 22 Sep 2018 12:02:33 -0700 Subject: [PATCH 261/544] v1.11.1 --- CHANGELOG.md | 9 +++++++++ setup.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f073ff16..9e62f335f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +1.11.1 +====== + +### Fixes +- Fix `.git` dir detection in `git<2.5` (regression introduced in + [1.10.5](#1105)) + - #831 issue by @mmacpherson. + - #832 PR by @asottile. + 1.11.0 ====== diff --git a/setup.py b/setup.py index 2c1390544..2ecc5fdb5 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.11.0', + version='1.11.1', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From 1b496c5fc37298d029c97782486ff420c99a4797 Mon Sep 17 00:00:00 2001 From: "George Y. Kussumoto" Date: Tue, 2 Oct 2018 12:17:46 -0300 Subject: [PATCH 262/544] Fix `check-useless-exclude` to consider types filter --- .../meta_hooks/check_useless_excludes.py | 5 ++- .../meta_hooks/check_useless_excludes_test.py | 31 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index cdc556df7..18b9f1637 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -9,6 +9,7 @@ from pre_commit import git from pre_commit.clientlib import load_config from pre_commit.clientlib import MANIFEST_HOOK_DICT +from pre_commit.commands.run import _filter_by_types def exclude_matches_any(filenames, include, exclude): @@ -39,8 +40,10 @@ def check_useless_excludes(config_file): # Not actually a manifest dict, but this more accurately reflects # the defaults applied during runtime hook = apply_defaults(hook, MANIFEST_HOOK_DICT) + types, exclude_types = hook['types'], hook['exclude_types'] + filtered_by_types = _filter_by_types(files, types, exclude_types) include, exclude = hook['files'], hook['exclude'] - if not exclude_matches_any(files, include, exclude): + if not exclude_matches_any(filtered_by_types, include, exclude): print( 'The exclude pattern {!r} for {} does not match any files' .format(exclude, hook['id']), diff --git a/tests/meta_hooks/check_useless_excludes_test.py b/tests/meta_hooks/check_useless_excludes_test.py index 1a03fb085..b2cc18731 100644 --- a/tests/meta_hooks/check_useless_excludes_test.py +++ b/tests/meta_hooks/check_useless_excludes_test.py @@ -51,6 +51,37 @@ def test_useless_exclude_for_hook(capsys, tempdir_factory): assert expected == out +def test_useless_exclude_with_types_filter(capsys, tempdir_factory): + config = { + 'repos': [ + { + 'repo': 'meta', + 'hooks': [ + { + 'id': 'check-useless-excludes', + 'exclude': '.pre-commit-config.yaml', + 'types': ['python'], + }, + ], + }, + ], + } + + repo = git_dir(tempdir_factory) + add_config_to_repo(repo, config) + + with cwd(repo): + assert check_useless_excludes.main(()) == 1 + + out, _ = capsys.readouterr() + out = out.strip() + expected = ( + "The exclude pattern '.pre-commit-config.yaml' for " + "check-useless-excludes does not match any files" + ) + assert expected == out + + def test_no_excludes(capsys, tempdir_factory): config = { 'repos': [ From fa4c03da655654a9818e3bcb95577a0eac4cf1ef Mon Sep 17 00:00:00 2001 From: "George Y. Kussumoto" Date: Fri, 5 Oct 2018 11:54:31 -0300 Subject: [PATCH 263/544] Update xargs.partition with platform information Change how xargs.partition computes the command length (including arguments) depending on the plataform. More specifically, 'win32' uses the amount of characters while posix system uses the byte count. --- pre_commit/xargs.py | 29 +++++++++++++++++++++-------- tests/xargs_test.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index eea3acdb9..1b237a381 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -1,41 +1,54 @@ from __future__ import absolute_import from __future__ import unicode_literals +import sys + from pre_commit import parse_shebang from pre_commit.util import cmd_output -# Limit used previously to avoid "xargs ... Bad file number" on windows -# This is slightly less than the posix mandated minimum -MAX_LENGTH = 4000 +# TODO: properly compute max_length value +def _get_platform_max_length(): + # posix minimum + return 4 * 1024 + + +def _get_command_length(command, arg): + parts = command + (arg,) + full_cmd = ' '.join(parts) + + # win32 uses the amount of characters, more details at: + # https://blogs.msdn.microsoft.com/oldnewthing/20031210-00/?p=41553/ + if sys.platform == 'win32': + return len(full_cmd) + + return len(full_cmd.encode(sys.getdefaultencoding())) class ArgumentTooLongError(RuntimeError): pass -def partition(cmd, varargs, _max_length=MAX_LENGTH): +def partition(cmd, varargs, _max_length=None): + _max_length = _max_length or _get_platform_max_length() cmd = tuple(cmd) ret = [] ret_cmd = [] - total_len = len(' '.join(cmd)) # Reversed so arguments are in order varargs = list(reversed(varargs)) while varargs: arg = varargs.pop() - if total_len + 1 + len(arg) <= _max_length: + if _get_command_length(cmd + tuple(ret_cmd), arg) <= _max_length: ret_cmd.append(arg) - total_len += len(arg) elif not ret_cmd: raise ArgumentTooLongError(arg) else: # We've exceeded the length, yield a command ret.append(cmd + tuple(ret_cmd)) ret_cmd = [] - total_len = len(' '.join(cmd)) varargs.append(arg) ret.append(cmd + tuple(ret_cmd)) diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 529eb197c..84d4899bc 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -1,11 +1,29 @@ from __future__ import absolute_import from __future__ import unicode_literals +from unittest import mock + import pytest from pre_commit import xargs +@pytest.fixture +def sys_win32_mock(): + return mock.Mock( + platform='win32', + getdefaultencoding=mock.Mock(return_value='utf-8'), + ) + + +@pytest.fixture +def sys_linux_mock(): + return mock.Mock( + platform='linux', + getdefaultencoding=mock.Mock(return_value='utf-8'), + ) + + def test_partition_trivial(): assert xargs.partition(('cmd',), ()) == (('cmd',),) @@ -35,6 +53,32 @@ def test_partition_limits(): ) +def test_partition_limit_win32(sys_win32_mock): + cmd = ('ninechars',) + varargs = ('😑' * 10,) + with mock.patch('pre_commit.xargs.sys', sys_win32_mock): + ret = xargs.partition(cmd, varargs, _max_length=20) + + assert ret == (cmd + varargs,) + + +def test_partition_limit_linux(sys_linux_mock): + cmd = ('ninechars',) + varargs = ('😑' * 5,) + with mock.patch('pre_commit.xargs.sys', sys_linux_mock): + ret = xargs.partition(cmd, varargs, _max_length=30) + + assert ret == (cmd + varargs,) + + +def test_argument_too_long_with_large_unicode(sys_linux_mock): + cmd = ('ninechars',) + varargs = ('😑' * 10,) # 4 bytes * 10 + with mock.patch('pre_commit.xargs.sys', sys_linux_mock): + with pytest.raises(xargs.ArgumentTooLongError): + xargs.partition(cmd, varargs, _max_length=20) + + def test_argument_too_long(): with pytest.raises(xargs.ArgumentTooLongError): xargs.partition(('a' * 5,), ('a' * 5,), _max_length=10) From df5d171cd7709db434f93474591aa867a69abe81 Mon Sep 17 00:00:00 2001 From: "George Y. Kussumoto" Date: Fri, 5 Oct 2018 14:33:32 -0300 Subject: [PATCH 264/544] Fix xargs.partition tests in python2.7 (pytest-mock) --- requirements-dev.txt | 1 + tests/xargs_test.py | 27 +++++++++++++-------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 157f287d3..bd7f84116 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,3 +5,4 @@ flake8 mock pytest pytest-env +pytest-mock diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 84d4899bc..e68f46c5f 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -1,26 +1,25 @@ +# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import unicode_literals -from unittest import mock - import pytest from pre_commit import xargs @pytest.fixture -def sys_win32_mock(): - return mock.Mock( +def sys_win32_mock(mocker): + return mocker.Mock( platform='win32', - getdefaultencoding=mock.Mock(return_value='utf-8'), + getdefaultencoding=mocker.Mock(return_value='utf-8'), ) @pytest.fixture -def sys_linux_mock(): - return mock.Mock( +def sys_linux_mock(mocker): + return mocker.Mock( platform='linux', - getdefaultencoding=mock.Mock(return_value='utf-8'), + getdefaultencoding=mocker.Mock(return_value='utf-8'), ) @@ -53,28 +52,28 @@ def test_partition_limits(): ) -def test_partition_limit_win32(sys_win32_mock): +def test_partition_limit_win32(mocker, sys_win32_mock): cmd = ('ninechars',) varargs = ('😑' * 10,) - with mock.patch('pre_commit.xargs.sys', sys_win32_mock): + with mocker.mock_module.patch('pre_commit.xargs.sys', sys_win32_mock): ret = xargs.partition(cmd, varargs, _max_length=20) assert ret == (cmd + varargs,) -def test_partition_limit_linux(sys_linux_mock): +def test_partition_limit_linux(mocker, sys_linux_mock): cmd = ('ninechars',) varargs = ('😑' * 5,) - with mock.patch('pre_commit.xargs.sys', sys_linux_mock): + with mocker.mock_module.patch('pre_commit.xargs.sys', sys_linux_mock): ret = xargs.partition(cmd, varargs, _max_length=30) assert ret == (cmd + varargs,) -def test_argument_too_long_with_large_unicode(sys_linux_mock): +def test_argument_too_long_with_large_unicode(mocker, sys_linux_mock): cmd = ('ninechars',) varargs = ('😑' * 10,) # 4 bytes * 10 - with mock.patch('pre_commit.xargs.sys', sys_linux_mock): + with mocker.mock_module.patch('pre_commit.xargs.sys', sys_linux_mock): with pytest.raises(xargs.ArgumentTooLongError): xargs.partition(cmd, varargs, _max_length=20) From 2ad69e12ce781c4b9242673893eacb3734d4afde Mon Sep 17 00:00:00 2001 From: "George Y. Kussumoto" Date: Fri, 5 Oct 2018 16:39:49 -0300 Subject: [PATCH 265/544] Fix xargs.partition: use sys.getfilesystemencoding The previous `sys.getdefaultencoding` almost always fallsback to `ascii` while `sys.getfilesystemencoding` is utf-8 once in utf-8 mode. --- pre_commit/xargs.py | 2 +- tests/xargs_test.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 1b237a381..2cbd6c395 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -22,7 +22,7 @@ def _get_command_length(command, arg): if sys.platform == 'win32': return len(full_cmd) - return len(full_cmd.encode(sys.getdefaultencoding())) + return len(full_cmd.encode(sys.getfilesystemencoding())) class ArgumentTooLongError(RuntimeError): diff --git a/tests/xargs_test.py b/tests/xargs_test.py index e68f46c5f..73ba9bc69 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -11,7 +11,7 @@ def sys_win32_mock(mocker): return mocker.Mock( platform='win32', - getdefaultencoding=mocker.Mock(return_value='utf-8'), + getfilesystemencoding=mocker.Mock(return_value='utf-8'), ) @@ -19,7 +19,7 @@ def sys_win32_mock(mocker): def sys_linux_mock(mocker): return mocker.Mock( platform='linux', - getdefaultencoding=mocker.Mock(return_value='utf-8'), + getfilesystemencoding=mocker.Mock(return_value='utf-8'), ) From bb6b1c33ae439889b08f82c49ab374f144d9a35b Mon Sep 17 00:00:00 2001 From: "George Y. Kussumoto" Date: Sat, 6 Oct 2018 19:57:30 -0300 Subject: [PATCH 266/544] Remove pytest-mock --- requirements-dev.txt | 1 - tests/xargs_test.py | 25 +++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index bd7f84116..157f287d3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,4 +5,3 @@ flake8 mock pytest pytest-env -pytest-mock diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 73ba9bc69..de16a0125 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -2,24 +2,25 @@ from __future__ import absolute_import from __future__ import unicode_literals +import mock import pytest from pre_commit import xargs @pytest.fixture -def sys_win32_mock(mocker): - return mocker.Mock( +def sys_win32_mock(): + return mock.Mock( platform='win32', - getfilesystemencoding=mocker.Mock(return_value='utf-8'), + getfilesystemencoding=mock.Mock(return_value='utf-8'), ) @pytest.fixture -def sys_linux_mock(mocker): - return mocker.Mock( +def sys_linux_mock(): + return mock.Mock( platform='linux', - getfilesystemencoding=mocker.Mock(return_value='utf-8'), + getfilesystemencoding=mock.Mock(return_value='utf-8'), ) @@ -52,28 +53,28 @@ def test_partition_limits(): ) -def test_partition_limit_win32(mocker, sys_win32_mock): +def test_partition_limit_win32(sys_win32_mock): cmd = ('ninechars',) varargs = ('😑' * 10,) - with mocker.mock_module.patch('pre_commit.xargs.sys', sys_win32_mock): + with mock.patch('pre_commit.xargs.sys', sys_win32_mock): ret = xargs.partition(cmd, varargs, _max_length=20) assert ret == (cmd + varargs,) -def test_partition_limit_linux(mocker, sys_linux_mock): +def test_partition_limit_linux(sys_linux_mock): cmd = ('ninechars',) varargs = ('😑' * 5,) - with mocker.mock_module.patch('pre_commit.xargs.sys', sys_linux_mock): + with mock.patch('pre_commit.xargs.sys', sys_linux_mock): ret = xargs.partition(cmd, varargs, _max_length=30) assert ret == (cmd + varargs,) -def test_argument_too_long_with_large_unicode(mocker, sys_linux_mock): +def test_argument_too_long_with_large_unicode(sys_linux_mock): cmd = ('ninechars',) varargs = ('😑' * 10,) # 4 bytes * 10 - with mocker.mock_module.patch('pre_commit.xargs.sys', sys_linux_mock): + with mock.patch('pre_commit.xargs.sys', sys_linux_mock): with pytest.raises(xargs.ArgumentTooLongError): xargs.partition(cmd, varargs, _max_length=20) From 333ea75e45e631d3c76521646f88a80333938b45 Mon Sep 17 00:00:00 2001 From: "George Y. Kussumoto" Date: Sat, 6 Oct 2018 20:04:17 -0300 Subject: [PATCH 267/544] Refactor xargs.partition: _command_length usage --- pre_commit/xargs.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 2cbd6c395..89a134d2b 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -13,9 +13,8 @@ def _get_platform_max_length(): return 4 * 1024 -def _get_command_length(command, arg): - parts = command + (arg,) - full_cmd = ' '.join(parts) +def _command_length(*cmd): + full_cmd = ' '.join(cmd) # win32 uses the amount of characters, more details at: # https://blogs.msdn.microsoft.com/oldnewthing/20031210-00/?p=41553/ @@ -38,17 +37,21 @@ def partition(cmd, varargs, _max_length=None): # Reversed so arguments are in order varargs = list(reversed(varargs)) + total_length = _command_length(*cmd) while varargs: arg = varargs.pop() - if _get_command_length(cmd + tuple(ret_cmd), arg) <= _max_length: + arg_length = _command_length(arg) + 1 + if total_length + arg_length <= _max_length: ret_cmd.append(arg) + total_length += arg_length elif not ret_cmd: raise ArgumentTooLongError(arg) else: # We've exceeded the length, yield a command ret.append(cmd + tuple(ret_cmd)) ret_cmd = [] + total_length = _command_length(*cmd) varargs.append(arg) ret.append(cmd + tuple(ret_cmd)) From 2560280d21bcdc646674214e24f7352861d7dcf8 Mon Sep 17 00:00:00 2001 From: "George Y. Kussumoto" Date: Mon, 8 Oct 2018 19:42:59 -0300 Subject: [PATCH 268/544] Fix xargs.partition tests: explicity set unicode chars --- tests/xargs_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/xargs_test.py b/tests/xargs_test.py index de16a0125..2d2a4ba27 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -55,7 +55,7 @@ def test_partition_limits(): def test_partition_limit_win32(sys_win32_mock): cmd = ('ninechars',) - varargs = ('😑' * 10,) + varargs = (u'😑' * 10,) with mock.patch('pre_commit.xargs.sys', sys_win32_mock): ret = xargs.partition(cmd, varargs, _max_length=20) @@ -64,7 +64,7 @@ def test_partition_limit_win32(sys_win32_mock): def test_partition_limit_linux(sys_linux_mock): cmd = ('ninechars',) - varargs = ('😑' * 5,) + varargs = (u'😑' * 5,) with mock.patch('pre_commit.xargs.sys', sys_linux_mock): ret = xargs.partition(cmd, varargs, _max_length=30) @@ -73,7 +73,7 @@ def test_partition_limit_linux(sys_linux_mock): def test_argument_too_long_with_large_unicode(sys_linux_mock): cmd = ('ninechars',) - varargs = ('😑' * 10,) # 4 bytes * 10 + varargs = (u'😑' * 10,) # 4 bytes * 10 with mock.patch('pre_commit.xargs.sys', sys_linux_mock): with pytest.raises(xargs.ArgumentTooLongError): xargs.partition(cmd, varargs, _max_length=20) From c9e297ddb62b30c19e2d6908cc6b0075823d83ee Mon Sep 17 00:00:00 2001 From: "George Y. Kussumoto" Date: Tue, 9 Oct 2018 22:54:41 -0300 Subject: [PATCH 269/544] Fix xargs.partition: win32 new string length computation --- pre_commit/xargs.py | 4 ++-- tests/xargs_test.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 89a134d2b..8a6320081 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -17,9 +17,9 @@ def _command_length(*cmd): full_cmd = ' '.join(cmd) # win32 uses the amount of characters, more details at: - # https://blogs.msdn.microsoft.com/oldnewthing/20031210-00/?p=41553/ + # https://github.com/pre-commit/pre-commit/pull/839 if sys.platform == 'win32': - return len(full_cmd) + return len(full_cmd.encode('utf-16le')) // 2 return len(full_cmd.encode(sys.getfilesystemencoding())) diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 2d2a4ba27..de16a0125 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -55,7 +55,7 @@ def test_partition_limits(): def test_partition_limit_win32(sys_win32_mock): cmd = ('ninechars',) - varargs = (u'😑' * 10,) + varargs = ('😑' * 10,) with mock.patch('pre_commit.xargs.sys', sys_win32_mock): ret = xargs.partition(cmd, varargs, _max_length=20) @@ -64,7 +64,7 @@ def test_partition_limit_win32(sys_win32_mock): def test_partition_limit_linux(sys_linux_mock): cmd = ('ninechars',) - varargs = (u'😑' * 5,) + varargs = ('😑' * 5,) with mock.patch('pre_commit.xargs.sys', sys_linux_mock): ret = xargs.partition(cmd, varargs, _max_length=30) @@ -73,7 +73,7 @@ def test_partition_limit_linux(sys_linux_mock): def test_argument_too_long_with_large_unicode(sys_linux_mock): cmd = ('ninechars',) - varargs = (u'😑' * 10,) # 4 bytes * 10 + varargs = ('😑' * 10,) # 4 bytes * 10 with mock.patch('pre_commit.xargs.sys', sys_linux_mock): with pytest.raises(xargs.ArgumentTooLongError): xargs.partition(cmd, varargs, _max_length=20) From 3d573d8736eb1bd7df39af7333dadc71da698d45 Mon Sep 17 00:00:00 2001 From: "George Y. Kussumoto" Date: Tue, 9 Oct 2018 23:32:46 -0300 Subject: [PATCH 270/544] Fix xargs.partion: win32 test --- tests/xargs_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/xargs_test.py b/tests/xargs_test.py index de16a0125..65336c58b 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -55,7 +55,8 @@ def test_partition_limits(): def test_partition_limit_win32(sys_win32_mock): cmd = ('ninechars',) - varargs = ('😑' * 10,) + # counted as half because of utf-16 encode + varargs = ('😑' * 5,) with mock.patch('pre_commit.xargs.sys', sys_win32_mock): ret = xargs.partition(cmd, varargs, _max_length=20) From 3181b461aa9386d733455147a1cac18dc50b6606 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 10 Oct 2018 20:08:16 -0700 Subject: [PATCH 271/544] fix pushing to new branch not identifying all commits --- pre_commit/resources/hook-tmpl | 11 ++++++----- tests/commands/install_uninstall_test.py | 2 ++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index cb25ec50e..f455ca35e 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -123,14 +123,15 @@ def _pre_push(stdin): elif remote_sha != Z40 and _rev_exists(remote_sha): opts = ('--origin', local_sha, '--source', remote_sha) else: - # First ancestor not found in remote - first_ancestor = subprocess.check_output(( - 'git', 'rev-list', '--max-count=1', '--topo-order', - '--reverse', local_sha, '--not', '--remotes={}'.format(remote), + # ancestors not found in remote + ancestors = subprocess.check_output(( + 'git', 'rev-list', local_sha, '--topo-order', '--reverse', + '--not', '--remotes={}'.format(remote), )).decode().strip() - if not first_ancestor: + if not ancestors: continue else: + first_ancestor = ancestors.splitlines()[0] cmd = ('git', 'rev-list', '--max-parents=0', local_sha) roots = set(subprocess.check_output(cmd).decode().splitlines()) if first_ancestor in roots: diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 40d9beead..76ab14f3d 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -527,11 +527,13 @@ def test_pre_push_integration_failing(tempdir_factory, store): install(Runner(path, C.CONFIG_FILE), store, hook_type='pre-push') # commit succeeds because pre-commit is only installed for pre-push assert _get_commit_output(tempdir_factory)[0] == 0 + assert _get_commit_output(tempdir_factory, touch_file='zzz')[0] == 0 retc, output = _get_push_output(tempdir_factory) assert retc == 1 assert 'Failing hook' in output assert 'Failed' in output + assert 'foo zzz' in output # both filenames should be printed assert 'hookid: failing_hook' in output From 9c374732566efa7883a85c53c5aa09d64214a6bd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 10 Oct 2018 20:43:57 -0700 Subject: [PATCH 272/544] v1.11.2 --- CHANGELOG.md | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e62f335f..49d5f80f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +1.11.2 +====== + +### Fixes +- `check-useless-exclude` now considers `types` + - #704 issue by @asottile. + - #837 PR by @georgeyk. +- `pre-push` hook was not identifying all commits on push to new branch + - #843 issue by @prem-nuro. + - #844 PR by @asottile. + 1.11.1 ====== diff --git a/setup.py b/setup.py index 2ecc5fdb5..3eb04d8a4 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.11.1', + version='1.11.2', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From 3dbb61d9af82700c2936f5f469334a82746384ca Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 12 Oct 2018 20:08:47 -0700 Subject: [PATCH 273/544] Migrate from autopep8-wrapper to mirrors-autopep8 Committed via https://github.com/asottile/all-repos --- .pre-commit-config.yaml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b5a9260f2..aa237a5eb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,9 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v1.4.0 + rev: v2.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - - id: autopep8-wrapper - id: check-docstring-first - id: check-json - id: check-yaml @@ -12,17 +11,21 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - id: flake8 +- repo: https://github.com/pre-commit/mirrors-autopep8 + rev: v1.4 + hooks: + - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v1.10.5 + rev: v1.11.2 hooks: - id: validate_manifest - repo: https://github.com/asottile/reorder_python_imports - rev: v1.1.0 + rev: v1.3.0 hooks: - id: reorder-python-imports language_version: python2.7 - repo: https://github.com/asottile/add-trailing-comma - rev: v0.6.4 + rev: v0.7.1 hooks: - id: add-trailing-comma - repo: meta From ebe5132576b9f84859e018b2430fa9a21e307716 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 14 Oct 2018 12:24:59 -0700 Subject: [PATCH 274/544] Replace pkg_resources.get_distribution with importlib-metadata --- pre_commit/constants.py | 5 ++--- pre_commit/repository.py | 8 ++++---- pre_commit/util.py | 5 +++++ setup.py | 2 ++ tests/util_test.py | 7 +++++++ 5 files changed, 20 insertions(+), 7 deletions(-) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 48ba2cb9c..a8cdc2e5c 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -1,7 +1,7 @@ from __future__ import absolute_import from __future__ import unicode_literals -import pkg_resources +import importlib_metadata # TODO: importlib.metadata py38? CONFIG_FILE = '.pre-commit-config.yaml' MANIFEST_FILE = '.pre-commit-hooks.yaml' @@ -18,8 +18,7 @@ # Bump when modifying `empty_template` LOCAL_REPO_VERSION = '1' -VERSION = pkg_resources.get_distribution('pre-commit').version -VERSION_PARSED = pkg_resources.parse_version(VERSION) +VERSION = importlib_metadata.version('pre_commit') # `manual` is not invoked by any installed git hook. See #719 STAGES = ('commit', 'commit-msg', 'manual', 'push') diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 278f31a2b..d718c2ff8 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -8,7 +8,6 @@ import shutil import sys -import pkg_resources from cached_property import cached_property from cfgv import apply_defaults from cfgv import validate @@ -23,6 +22,7 @@ from pre_commit.languages.all import languages from pre_commit.languages.helpers import environment_dir from pre_commit.prefix import Prefix +from pre_commit.util import parse_version logger = logging.getLogger('pre_commit') @@ -110,13 +110,13 @@ def _hook(*hook_dicts): for dct in rest: ret.update(dct) - version = pkg_resources.parse_version(ret['minimum_pre_commit_version']) - if version > C.VERSION_PARSED: + version = ret['minimum_pre_commit_version'] + if parse_version(version) > parse_version(C.VERSION): logger.error( 'The hook `{}` requires pre-commit version {} but version {} ' 'is installed. ' 'Perhaps run `pip install --upgrade pre-commit`.'.format( - ret['id'], version, C.VERSION_PARSED, + ret['id'], version, C.VERSION, ), ) exit(1) diff --git a/pre_commit/util.py b/pre_commit/util.py index bcb47c3fc..55210f104 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -211,3 +211,8 @@ def copy_tree_to_path(src_dir, dest_dir): shutil.copytree(srcname, destname) else: shutil.copy(srcname, destname) + + +def parse_version(s): + """poor man's version comparison""" + return tuple(int(p) for p in s.split('.')) diff --git a/setup.py b/setup.py index 3eb04d8a4..82a703715 100644 --- a/setup.py +++ b/setup.py @@ -40,6 +40,8 @@ 'cached-property', 'cfgv>=1.0.0', 'identify>=1.0.0', + # if this makes it into python3.8 move to extras_require + 'importlib-metadata', 'nodeenv>=0.11.1', 'pyyaml', 'six', diff --git a/tests/util_test.py b/tests/util_test.py index 967163e46..56eb5aaa2 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -9,6 +9,7 @@ from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import memoize_by_cwd +from pre_commit.util import parse_version from pre_commit.util import tmpdir from testing.util import cwd @@ -117,3 +118,9 @@ def test_cmd_output_exe_not_found(): ret, out, _ = cmd_output('i-dont-exist', retcode=None) assert ret == 1 assert out == 'Executable `i-dont-exist` not found' + + +def test_parse_version(): + assert parse_version('0.0') == parse_version('0.0') + assert parse_version('0.1') > parse_version('0.0') + assert parse_version('2.1') >= parse_version('2') From 9f60561d6f5038cf58d3cd1dd6f2baccfc630f0d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 14 Oct 2018 13:17:38 -0700 Subject: [PATCH 275/544] Replace resources with importlib_resources --- pre_commit/commands/install_uninstall.py | 5 +-- pre_commit/languages/ruby.py | 21 ++++++----- pre_commit/make_archives.py | 3 +- pre_commit/resources/__init__.py | 0 .../.npmignore => empty_template_.npmignore} | 0 .../Cargo.toml => empty_template_Cargo.toml} | 0 .../main.go => empty_template_main.go} | 0 .../main.rs => empty_template_main.rs} | 0 ...ckage.json => empty_template_package.json} | 0 ...template_pre_commit_dummy_package.gemspec} | 0 .../setup.py => empty_template_setup.py} | 0 pre_commit/store.py | 13 +++++-- pre_commit/util.py | 37 +++++++------------ setup.py | 6 +-- testing/fixtures.py | 20 +++++++++- tests/commands/install_uninstall_test.py | 7 ++-- tests/store_test.py | 9 +++++ 17 files changed, 72 insertions(+), 49 deletions(-) create mode 100644 pre_commit/resources/__init__.py rename pre_commit/resources/{empty_template/.npmignore => empty_template_.npmignore} (100%) rename pre_commit/resources/{empty_template/Cargo.toml => empty_template_Cargo.toml} (100%) rename pre_commit/resources/{empty_template/main.go => empty_template_main.go} (100%) rename pre_commit/resources/{empty_template/main.rs => empty_template_main.rs} (100%) rename pre_commit/resources/{empty_template/package.json => empty_template_package.json} (100%) rename pre_commit/resources/{empty_template/pre_commit_dummy_package.gemspec => empty_template_pre_commit_dummy_package.gemspec} (100%) rename pre_commit/resources/{empty_template/setup.py => empty_template_setup.py} (100%) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index d76a6c1a4..d31330603 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -12,7 +12,7 @@ from pre_commit.util import cmd_output from pre_commit.util import make_executable from pre_commit.util import mkdirp -from pre_commit.util import resource_filename +from pre_commit.util import resource_text logger = logging.getLogger(__name__) @@ -80,8 +80,7 @@ def install( } with io.open(hook_path, 'w') as hook_file: - with io.open(resource_filename('hook-tmpl')) as f: - contents = f.read() + contents = resource_text('hook-tmpl') before, rest = contents.split(TEMPLATE_START) to_template, after = rest.split(TEMPLATE_END) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 3bd7130d1..bef3fe387 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -11,7 +11,7 @@ from pre_commit.languages import helpers from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure -from pre_commit.util import resource_filename +from pre_commit.util import resource_bytesio from pre_commit.xargs import xargs @@ -47,22 +47,23 @@ def in_env(prefix, language_version): # pragma: windows no cover yield +def _extract_resource(filename, dest): + with resource_bytesio(filename) as bio: + with tarfile.open(fileobj=bio) as tf: + tf.extractall(dest) + + def _install_rbenv(prefix, version='default'): # pragma: windows no cover directory = helpers.environment_dir(ENVIRONMENT_DIR, version) - with tarfile.open(resource_filename('rbenv.tar.gz')) as tf: - tf.extractall(prefix.path('.')) + _extract_resource('rbenv.tar.gz', prefix.path('.')) shutil.move(prefix.path('rbenv'), prefix.path(directory)) # Only install ruby-build if the version is specified if version != 'default': - # ruby-download - with tarfile.open(resource_filename('ruby-download.tar.gz')) as tf: - tf.extractall(prefix.path(directory, 'plugins')) - - # ruby-build - with tarfile.open(resource_filename('ruby-build.tar.gz')) as tf: - tf.extractall(prefix.path(directory, 'plugins')) + plugins_dir = prefix.path(directory, 'plugins') + _extract_resource('ruby-download.tar.gz', plugins_dir) + _extract_resource('ruby-build.tar.gz', plugins_dir) activate_path = prefix.path(directory, 'bin', 'activate') with io.open(activate_path, 'w') as activate_file: diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index e85a8f4a6..865ef0615 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -8,7 +8,6 @@ from pre_commit import output from pre_commit.util import cmd_output -from pre_commit.util import resource_filename from pre_commit.util import rmtree from pre_commit.util import tmpdir @@ -56,7 +55,7 @@ def make_archive(name, repo, ref, destdir): def main(argv=None): parser = argparse.ArgumentParser() - parser.add_argument('--dest', default=resource_filename()) + parser.add_argument('--dest', default='pre_commit/resources') args = parser.parse_args(argv) for archive_name, repo, ref in REPOS: output.write_line('Making {}.tar.gz for {}@{}'.format( diff --git a/pre_commit/resources/__init__.py b/pre_commit/resources/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pre_commit/resources/empty_template/.npmignore b/pre_commit/resources/empty_template_.npmignore similarity index 100% rename from pre_commit/resources/empty_template/.npmignore rename to pre_commit/resources/empty_template_.npmignore diff --git a/pre_commit/resources/empty_template/Cargo.toml b/pre_commit/resources/empty_template_Cargo.toml similarity index 100% rename from pre_commit/resources/empty_template/Cargo.toml rename to pre_commit/resources/empty_template_Cargo.toml diff --git a/pre_commit/resources/empty_template/main.go b/pre_commit/resources/empty_template_main.go similarity index 100% rename from pre_commit/resources/empty_template/main.go rename to pre_commit/resources/empty_template_main.go diff --git a/pre_commit/resources/empty_template/main.rs b/pre_commit/resources/empty_template_main.rs similarity index 100% rename from pre_commit/resources/empty_template/main.rs rename to pre_commit/resources/empty_template_main.rs diff --git a/pre_commit/resources/empty_template/package.json b/pre_commit/resources/empty_template_package.json similarity index 100% rename from pre_commit/resources/empty_template/package.json rename to pre_commit/resources/empty_template_package.json diff --git a/pre_commit/resources/empty_template/pre_commit_dummy_package.gemspec b/pre_commit/resources/empty_template_pre_commit_dummy_package.gemspec similarity index 100% rename from pre_commit/resources/empty_template/pre_commit_dummy_package.gemspec rename to pre_commit/resources/empty_template_pre_commit_dummy_package.gemspec diff --git a/pre_commit/resources/empty_template/setup.py b/pre_commit/resources/empty_template_setup.py similarity index 100% rename from pre_commit/resources/empty_template/setup.py rename to pre_commit/resources/empty_template_setup.py diff --git a/pre_commit/store.py b/pre_commit/store.py index 07702fb5d..f3096fcd0 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -11,9 +11,8 @@ from pre_commit import file_lock from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output -from pre_commit.util import copy_tree_to_path from pre_commit.util import no_git_env -from pre_commit.util import resource_filename +from pre_commit.util import resource_text logger = logging.getLogger('pre_commit') @@ -149,9 +148,17 @@ def _git_cmd(*args): return self._new_repo(repo, ref, deps, clone_strategy) + LOCAL_RESOURCES = ( + 'Cargo.toml', 'main.go', 'main.rs', '.npmignore', 'package.json', + 'pre_commit_dummy_package.gemspec', 'setup.py', + ) + def make_local(self, deps): def make_local_strategy(directory): - copy_tree_to_path(resource_filename('empty_template'), directory) + for resource in self.LOCAL_RESOURCES: + contents = resource_text('empty_template_{}'.format(resource)) + with io.open(os.path.join(directory, resource), 'w') as f: + f.write(contents) env = no_git_env() name, email = 'pre-commit', 'asottile+pre-commit@umich.edu' diff --git a/pre_commit/util.py b/pre_commit/util.py index 55210f104..963461d16 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -7,14 +7,21 @@ import shutil import stat import subprocess +import sys import tempfile -import pkg_resources import six from pre_commit import five from pre_commit import parse_shebang +if sys.version_info >= (3, 7): # pragma: no cover (PY37+) + from importlib.resources import open_binary + from importlib.resources import read_text +else: # pragma: no cover ( Date: Sun, 14 Oct 2018 13:41:59 -0700 Subject: [PATCH 276/544] Exclude coverage in the template file --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 2dca7634c..d7a248121 100644 --- a/.coveragerc +++ b/.coveragerc @@ -8,6 +8,7 @@ omit = # Don't complain if non-runnable code isn't run */__main__.py pre_commit/color_windows.py + pre_commit/resources/* [report] show_missing = True From 8e8b9622660f3cac7bece0c1007bf3604de0103d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 14 Oct 2018 14:59:36 -0700 Subject: [PATCH 277/544] Improve coverage of check_hooks_apply --- tests/meta_hooks/check_hooks_apply_test.py | 44 +++++++++------------- 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/tests/meta_hooks/check_hooks_apply_test.py b/tests/meta_hooks/check_hooks_apply_test.py index c75b036a3..d48d9d7af 100644 --- a/tests/meta_hooks/check_hooks_apply_test.py +++ b/tests/meta_hooks/check_hooks_apply_test.py @@ -106,39 +106,20 @@ def test_hook_types_excludes_everything( assert 'check-useless-excludes does not apply to this repository' in out -def test_valid_always_run(capsys, tempdir_factory, mock_store_dir): +def test_valid_exceptions(capsys, tempdir_factory, mock_store_dir): config = { 'repos': [ { - 'repo': 'meta', + 'repo': 'local', 'hooks': [ - # Should not be reported as an error due to always_run + # applies to a file { - 'id': 'check-useless-excludes', - 'files': '^$', - 'always_run': True, + 'id': 'check-yaml', + 'name': 'check yaml', + 'entry': './check-yaml', + 'language': 'script', + 'files': r'\.yaml$', }, - ], - }, - ], - } - - repo = git_dir(tempdir_factory) - add_config_to_repo(repo, config) - - with cwd(repo): - assert check_hooks_apply.main(()) == 0 - - out, _ = capsys.readouterr() - assert out == '' - - -def test_valid_language_fail(capsys, tempdir_factory, mock_store_dir): - config = { - 'repos': [ - { - 'repo': 'local', - 'hooks': [ # Should not be reported as an error due to language: fail { 'id': 'changelogs-rst', @@ -147,6 +128,15 @@ def test_valid_language_fail(capsys, tempdir_factory, mock_store_dir): 'language': 'fail', 'files': r'changelog/.*(? Date: Tue, 23 Oct 2018 10:17:21 -0700 Subject: [PATCH 278/544] Install multi-hook repositories only once --- pre_commit/prefix.py | 6 +++--- pre_commit/repository.py | 6 +++--- tests/repository_test.py | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pre_commit/prefix.py b/pre_commit/prefix.py index 073b3f542..f8a8a9d69 100644 --- a/pre_commit/prefix.py +++ b/pre_commit/prefix.py @@ -1,11 +1,11 @@ from __future__ import unicode_literals +import collections import os.path -class Prefix(object): - def __init__(self, prefix_dir): - self.prefix_dir = prefix_dir +class Prefix(collections.namedtuple('Prefix', ('prefix_dir',))): + __slots__ = () def path(self, *parts): return os.path.normpath(os.path.join(self.prefix_dir, *parts)) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index d718c2ff8..2a4355069 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -179,12 +179,12 @@ def _prefix_from_deps(self, language_name, deps): return Prefix(self.store.clone(repo, rev, deps)) def _venvs(self): - ret = [] + ret = set() for _, hook in self.hooks: language = hook['language'] version = hook['language_version'] - deps = hook['additional_dependencies'] - ret.append(( + deps = tuple(hook['additional_dependencies']) + ret.add(( self._prefix_from_deps(language, deps), language, version, deps, )) diff --git a/tests/repository_test.py b/tests/repository_test.py index 4c76f9a07..8d578f39b 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -466,7 +466,7 @@ def test_venvs(tempdir_factory, store): config = make_config_from_repo(path) repo = Repository.create(config, store) venv, = repo._venvs() - assert venv == (mock.ANY, 'python', python.get_default_version(), []) + assert venv == (mock.ANY, 'python', python.get_default_version(), ()) def test_additional_dependencies(tempdir_factory, store): @@ -474,8 +474,8 @@ def test_additional_dependencies(tempdir_factory, store): config = make_config_from_repo(path) config['hooks'][0]['additional_dependencies'] = ['pep8'] repo = Repository.create(config, store) - venv, = repo._venvs() - assert venv == (mock.ANY, 'python', python.get_default_version(), ['pep8']) + env, = repo._venvs() + assert env == (mock.ANY, 'python', python.get_default_version(), ('pep8',)) def test_additional_dependencies_roll_forward(tempdir_factory, store): From 0c9a53bf1b48753bc6748133766deaeca86182e8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 23 Oct 2018 10:50:35 -0700 Subject: [PATCH 279/544] Correct resources declaration --- setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 8994da680..e831faf37 100644 --- a/setup.py +++ b/setup.py @@ -28,10 +28,10 @@ ], packages=find_packages(exclude=('tests*', 'testing*')), package_data={ - 'pre_commit': [ - 'resources/hook-tmpl', - 'resources/*.tar.gz', - 'resources/empty_template_*', + 'pre_commit.resources': [ + '*.tar.gz', + 'empty_template_*', + 'hook-tmpl', ], }, install_requires=[ From eecf3472ffa1ce8e8f4638956d319820d80bdf54 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 23 Oct 2018 10:55:09 -0700 Subject: [PATCH 280/544] v1.12.0 --- CHANGELOG.md | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49d5f80f8..355b08248 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +1.12.0 +====== + +### Fixes +- Install multi-hook repositories only once (performance) + - issue by @chriskuehl. + - #852 PR by @asottile. +- Improve performance by factoring out pkg_resources (performance) + - #840 issue by @RonnyPfannschmidt. + - #846 PR by @asottile. + 1.11.2 ====== diff --git a/setup.py b/setup.py index e831faf37..7c0a958f4 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.11.2', + version='1.12.0', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From ead906aed066d66c216308a891e93596e85ec09c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 23 Oct 2018 22:02:48 -0700 Subject: [PATCH 281/544] Compute win32 python2 length according to encoded size --- pre_commit/xargs.py | 12 +++++++--- tests/xargs_test.py | 54 +++++++++++++++++++++++++++------------------ 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 8a6320081..2fe8a4549 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -3,6 +3,8 @@ import sys +import six + from pre_commit import parse_shebang from pre_commit.util import cmd_output @@ -19,9 +21,13 @@ def _command_length(*cmd): # win32 uses the amount of characters, more details at: # https://github.com/pre-commit/pre-commit/pull/839 if sys.platform == 'win32': - return len(full_cmd.encode('utf-16le')) // 2 - - return len(full_cmd.encode(sys.getfilesystemencoding())) + # the python2.x apis require bytes, we encode as UTF-8 + if six.PY2: + return len(full_cmd.encode('utf-8')) + else: + return len(full_cmd.encode('utf-16le')) // 2 + else: + return len(full_cmd.encode(sys.getfilesystemencoding())) class ArgumentTooLongError(RuntimeError): diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 65336c58b..bf685e16a 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -2,26 +2,36 @@ from __future__ import absolute_import from __future__ import unicode_literals +import sys + import mock import pytest +import six from pre_commit import xargs @pytest.fixture -def sys_win32_mock(): - return mock.Mock( - platform='win32', - getfilesystemencoding=mock.Mock(return_value='utf-8'), - ) +def win32_py2_mock(): + with mock.patch.object(sys, 'getfilesystemencoding', return_value='utf-8'): + with mock.patch.object(sys, 'platform', 'win32'): + with mock.patch.object(six, 'PY2', True): + yield @pytest.fixture -def sys_linux_mock(): - return mock.Mock( - platform='linux', - getfilesystemencoding=mock.Mock(return_value='utf-8'), - ) +def win32_py3_mock(): + with mock.patch.object(sys, 'getfilesystemencoding', return_value='utf-8'): + with mock.patch.object(sys, 'platform', 'win32'): + with mock.patch.object(six, 'PY2', False): + yield + + +@pytest.fixture +def linux_mock(): + with mock.patch.object(sys, 'getfilesystemencoding', return_value='utf-8'): + with mock.patch.object(sys, 'platform', 'linux'): + yield def test_partition_trivial(): @@ -53,31 +63,33 @@ def test_partition_limits(): ) -def test_partition_limit_win32(sys_win32_mock): +def test_partition_limit_win32_py3(win32_py3_mock): cmd = ('ninechars',) # counted as half because of utf-16 encode varargs = ('😑' * 5,) - with mock.patch('pre_commit.xargs.sys', sys_win32_mock): - ret = xargs.partition(cmd, varargs, _max_length=20) + ret = xargs.partition(cmd, varargs, _max_length=20) + assert ret == (cmd + varargs,) + +def test_partition_limit_win32_py2(win32_py2_mock): + cmd = ('ninechars',) + varargs = ('😑' * 5,) # 4 bytes * 5 + ret = xargs.partition(cmd, varargs, _max_length=30) assert ret == (cmd + varargs,) -def test_partition_limit_linux(sys_linux_mock): +def test_partition_limit_linux(linux_mock): cmd = ('ninechars',) varargs = ('😑' * 5,) - with mock.patch('pre_commit.xargs.sys', sys_linux_mock): - ret = xargs.partition(cmd, varargs, _max_length=30) - + ret = xargs.partition(cmd, varargs, _max_length=30) assert ret == (cmd + varargs,) -def test_argument_too_long_with_large_unicode(sys_linux_mock): +def test_argument_too_long_with_large_unicode(linux_mock): cmd = ('ninechars',) varargs = ('😑' * 10,) # 4 bytes * 10 - with mock.patch('pre_commit.xargs.sys', sys_linux_mock): - with pytest.raises(xargs.ArgumentTooLongError): - xargs.partition(cmd, varargs, _max_length=20) + with pytest.raises(xargs.ArgumentTooLongError): + xargs.partition(cmd, varargs, _max_length=20) def test_argument_too_long(): From ba5e27e4ec087f80e07c646c365578d63ee39ee9 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Sat, 20 Oct 2018 13:05:55 -0700 Subject: [PATCH 282/544] Implement concurrent execution of individual hooks --- pre_commit/clientlib.py | 1 + pre_commit/languages/docker.py | 6 +++++- pre_commit/languages/docker_image.py | 6 +++++- pre_commit/languages/golang.py | 6 +++++- pre_commit/languages/helpers.py | 9 +++++++++ pre_commit/languages/node.py | 6 +++++- pre_commit/languages/python.py | 6 +++++- pre_commit/languages/ruby.py | 6 +++++- pre_commit/languages/rust.py | 6 +++++- pre_commit/languages/script.py | 6 +++++- pre_commit/languages/swift.py | 6 +++++- pre_commit/languages/system.py | 6 +++++- pre_commit/xargs.py | 29 ++++++++++++++++++++++++---- tests/repository_test.py | 1 + tests/xargs_test.py | 18 +++++++++++++++++ 15 files changed, 104 insertions(+), 14 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 4570e1079..2fa7b1535 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -56,6 +56,7 @@ def _make_argparser(filenames_help): cfgv.Optional('language_version', cfgv.check_string, 'default'), cfgv.Optional('log_file', cfgv.check_string, ''), cfgv.Optional('minimum_pre_commit_version', cfgv.check_string, '0'), + cfgv.Optional('require_serial', cfgv.check_bool, False), cfgv.Optional('stages', cfgv.check_array(cfgv.check_one_of(C.STAGES)), []), cfgv.Optional('verbose', cfgv.check_bool, False), ) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index f3c46a33d..7f00fe60e 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -97,4 +97,8 @@ def run_hook(prefix, hook, file_args): # pragma: windows no cover entry_tag = ('--entrypoint', entry_exe, docker_tag(prefix)) cmd = docker_cmd() + entry_tag + cmd_rest - return xargs(cmd, file_args) + return xargs( + cmd, + file_args, + target_concurrency=helpers.target_concurrency(hook), + ) diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index 6301970c4..e990f18aa 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -16,4 +16,8 @@ def run_hook(prefix, hook, file_args): # pragma: windows no cover assert_docker_available() cmd = docker_cmd() + helpers.to_cmd(hook) - return xargs(cmd, file_args) + return xargs( + cmd, + file_args, + target_concurrency=helpers.target_concurrency(hook), + ) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 14354e0ce..7d273e752 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -81,4 +81,8 @@ def install_environment(prefix, version, additional_dependencies): def run_hook(prefix, hook, file_args): with in_env(prefix): - return xargs(helpers.to_cmd(hook), file_args) + return xargs( + helpers.to_cmd(hook), + file_args, + target_concurrency=helpers.target_concurrency(hook), + ) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index ddbe2e80e..b6a3fc2d5 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import multiprocessing import shlex from pre_commit.util import cmd_output @@ -45,3 +46,11 @@ def basic_healthy(prefix, language_version): def no_install(prefix, version, additional_dependencies): raise AssertionError('This type is not installable') + + +def target_concurrency(hook): + if hook['require_serial']: + return 1 + else: + # TODO: something smart! + return multiprocessing.cpu_count() diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 7b4649302..494ca878e 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -71,4 +71,8 @@ def install_environment(prefix, version, additional_dependencies): def run_hook(prefix, hook, file_args): with in_env(prefix, hook['language_version']): - return xargs(helpers.to_cmd(hook), file_args) + return xargs( + helpers.to_cmd(hook), + file_args, + target_concurrency=helpers.target_concurrency(hook), + ) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index ee7b2a4f1..bb8a81a66 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -127,7 +127,11 @@ def healthy(prefix, language_version): def run_hook(prefix, hook, file_args): with in_env(prefix, hook['language_version']): - return xargs(helpers.to_cmd(hook), file_args) + return xargs( + helpers.to_cmd(hook), + file_args, + target_concurrency=helpers.target_concurrency(hook), + ) def install_environment(prefix, version, additional_dependencies): additional_dependencies = tuple(additional_dependencies) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index bef3fe387..3c5745df1 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -126,4 +126,8 @@ def install_environment( def run_hook(prefix, hook, file_args): # pragma: windows no cover with in_env(prefix, hook['language_version']): - return xargs(helpers.to_cmd(hook), file_args) + return xargs( + helpers.to_cmd(hook), + file_args, + target_concurrency=helpers.target_concurrency(hook), + ) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 41053f889..e602adcc6 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -91,4 +91,8 @@ def install_environment(prefix, version, additional_dependencies): def run_hook(prefix, hook, file_args): with in_env(prefix): - return xargs(helpers.to_cmd(hook), file_args) + return xargs( + helpers.to_cmd(hook), + file_args, + target_concurrency=helpers.target_concurrency(hook), + ) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 551b4d80e..d242694f5 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -13,4 +13,8 @@ def run_hook(prefix, hook, file_args): cmd = helpers.to_cmd(hook) cmd = (prefix.path(cmd[0]),) + cmd[1:] - return xargs(cmd, file_args) + return xargs( + cmd, + file_args, + target_concurrency=helpers.target_concurrency(hook), + ) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 2863fbee7..eff4f9b07 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -53,4 +53,8 @@ def install_environment( def run_hook(prefix, hook, file_args): # pragma: windows no cover with in_env(prefix): - return xargs(helpers.to_cmd(hook), file_args) + return xargs( + helpers.to_cmd(hook), + file_args, + target_concurrency=helpers.target_concurrency(hook), + ) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 84cd1fe4e..70a42ddcd 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -11,4 +11,8 @@ def run_hook(prefix, hook, file_args): - return xargs(helpers.to_cmd(hook), file_args) + return xargs( + helpers.to_cmd(hook), + file_args, + target_concurrency=helpers.target_concurrency(hook), + ) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 2fe8a4549..aa4f27e08 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -1,8 +1,11 @@ from __future__ import absolute_import from __future__ import unicode_literals +import contextlib +import multiprocessing.pool import sys +import concurrent.futures import six from pre_commit import parse_shebang @@ -65,12 +68,23 @@ def partition(cmd, varargs, _max_length=None): return tuple(ret) +@contextlib.contextmanager +def _threadpool(size): + pool = multiprocessing.pool.ThreadPool(size) + try: + yield pool + finally: + pool.terminate() + + def xargs(cmd, varargs, **kwargs): """A simplified implementation of xargs. negate: Make nonzero successful and zero a failure + target_concurrency: Target number of partitions to run concurrently """ negate = kwargs.pop('negate', False) + target_concurrency = kwargs.pop('target_concurrency', 1) retcode = 0 stdout = b'' stderr = b'' @@ -80,10 +94,17 @@ def xargs(cmd, varargs, **kwargs): except parse_shebang.ExecutableNotFoundError as e: return e.to_output() - for run_cmd in partition(cmd, varargs, **kwargs): - proc_retcode, proc_out, proc_err = cmd_output( - *run_cmd, encoding=None, retcode=None - ) + # TODO: teach partition to intelligently target our desired concurrency + # while still respecting max_length. + partitions = partition(cmd, varargs, **kwargs) + + def run_cmd_partition(run_cmd): + return cmd_output(*run_cmd, encoding=None, retcode=None) + + with _threadpool(min(len(partitions), target_concurrency)) as pool: + results = pool.map(run_cmd_partition, partitions) + + for proc_retcode, proc_out, proc_err in results: # This is *slightly* too clever so I'll explain it. # First the xor boolean table: # T | F | diff --git a/tests/repository_test.py b/tests/repository_test.py index 8d578f39b..f1b0f6e02 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -837,6 +837,7 @@ def test_manifest_hooks(tempdir_factory, store): 'minimum_pre_commit_version': '0', 'name': 'Bash hook', 'pass_filenames': True, + 'require_serial': False, 'stages': [], 'types': ['file'], 'exclude_types': [], diff --git a/tests/xargs_test.py b/tests/xargs_test.py index bf685e16a..b60a37d64 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import sys +import time import mock import pytest @@ -132,3 +133,20 @@ def test_xargs_retcode_normal(): ret, _, _ = xargs.xargs(exit_cmd, ('0', '1'), _max_length=max_length) assert ret == 1 + + +def test_xargs_concurrency(): + bash_cmd = ('bash', '-c') + print_pid = ('sleep 0.5 && echo $$',) + + start = time.time() + ret, stdout, _ = xargs.xargs( + bash_cmd, print_pid * 5, + target_concurrency=5, + _max_length=len(' '.join(bash_cmd + print_pid)), + ) + elapsed = time.time() - start + assert ret == 0 + pids = stdout.splitlines() + assert len(pids) == 5 + assert elapsed < 1 From ec0ed8aef5a904becf5facde6d90045a6f90e6cd Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Sat, 20 Oct 2018 17:13:57 -0700 Subject: [PATCH 283/544] Handle CPU detection errors and running on Travis --- pre_commit/languages/helpers.py | 11 +++++++++-- tests/languages/helpers_test.py | 25 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index b6a3fc2d5..abd28fa07 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import multiprocessing +import os import shlex from pre_commit.util import cmd_output @@ -52,5 +53,11 @@ def target_concurrency(hook): if hook['require_serial']: return 1 else: - # TODO: something smart! - return multiprocessing.cpu_count() + # Travis appears to have a bunch of CPUs, but we can't use them all. + if 'TRAVIS' in os.environ: + return 2 + else: + try: + return multiprocessing.cpu_count() + except NotImplementedError: + return 1 diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index ada2095b6..f1c1497f7 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -1,8 +1,11 @@ from __future__ import absolute_import from __future__ import unicode_literals +import multiprocessing +import os import sys +import mock import pytest from pre_commit.languages import helpers @@ -28,3 +31,25 @@ def test_failed_setup_command_does_not_unicode_error(): # an assertion that this does not raise `UnicodeError` with pytest.raises(CalledProcessError): helpers.run_setup_cmd(Prefix('.'), (sys.executable, '-c', script)) + + +def test_target_concurrency_normal(): + with mock.patch.object(multiprocessing, 'cpu_count', return_value=123): + with mock.patch.dict(os.environ, {}, clear=True): + assert helpers.target_concurrency({'require_serial': False}) == 123 + + +def test_target_concurrency_cpu_count_require_serial_true(): + assert helpers.target_concurrency({'require_serial': True}) == 1 + + +def test_target_concurrency_on_travis(): + with mock.patch.dict(os.environ, {'TRAVIS': '1'}, clear=True): + assert helpers.target_concurrency({'require_serial': False}) == 2 + + +def test_target_concurrency_cpu_count_not_implemented(): + with mock.patch.object( + multiprocessing, 'cpu_count', side_effect=NotImplementedError, + ): + assert helpers.target_concurrency({'require_serial': False}) == 1 From b6926e8e2ef50d709945f75252e7c6b9cacda290 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Sat, 20 Oct 2018 17:14:50 -0700 Subject: [PATCH 284/544] Attempt to partition files to use all possible cores --- pre_commit/xargs.py | 18 +++++++++++++----- tests/xargs_test.py | 42 +++++++++++++++++++++++++++++++++++------- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index aa4f27e08..9c4bc78ac 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -1,7 +1,9 @@ from __future__ import absolute_import +from __future__ import division from __future__ import unicode_literals import contextlib +import math import multiprocessing.pool import sys @@ -37,8 +39,13 @@ class ArgumentTooLongError(RuntimeError): pass -def partition(cmd, varargs, _max_length=None): +def partition(cmd, varargs, target_concurrency, _max_length=None): _max_length = _max_length or _get_platform_max_length() + + # Generally, we try to partition evenly into at least `target_concurrency` + # partitions, but we don't want a bunch of tiny partitions. + max_args = max(4, math.ceil(len(varargs) / target_concurrency)) + cmd = tuple(cmd) ret = [] @@ -51,7 +58,10 @@ def partition(cmd, varargs, _max_length=None): arg = varargs.pop() arg_length = _command_length(arg) + 1 - if total_length + arg_length <= _max_length: + if ( + total_length + arg_length <= _max_length + and len(ret_cmd) < max_args + ): ret_cmd.append(arg) total_length += arg_length elif not ret_cmd: @@ -94,9 +104,7 @@ def xargs(cmd, varargs, **kwargs): except parse_shebang.ExecutableNotFoundError as e: return e.to_output() - # TODO: teach partition to intelligently target our desired concurrency - # while still respecting max_length. - partitions = partition(cmd, varargs, **kwargs) + partitions = partition(cmd, varargs, target_concurrency, **kwargs) def run_cmd_partition(run_cmd): return cmd_output(*run_cmd, encoding=None, retcode=None) diff --git a/tests/xargs_test.py b/tests/xargs_test.py index b60a37d64..3dcb6e8a1 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -36,11 +36,11 @@ def linux_mock(): def test_partition_trivial(): - assert xargs.partition(('cmd',), ()) == (('cmd',),) + assert xargs.partition(('cmd',), (), 1) == (('cmd',),) def test_partition_simple(): - assert xargs.partition(('cmd',), ('foo',)) == (('cmd', 'foo'),) + assert xargs.partition(('cmd',), ('foo',), 1) == (('cmd', 'foo'),) def test_partition_limits(): @@ -54,6 +54,7 @@ def test_partition_limits(): '.' * 5, '.' * 6, ), + 1, _max_length=20, ) assert ret == ( @@ -68,21 +69,21 @@ def test_partition_limit_win32_py3(win32_py3_mock): cmd = ('ninechars',) # counted as half because of utf-16 encode varargs = ('😑' * 5,) - ret = xargs.partition(cmd, varargs, _max_length=20) + ret = xargs.partition(cmd, varargs, 1, _max_length=20) assert ret == (cmd + varargs,) def test_partition_limit_win32_py2(win32_py2_mock): cmd = ('ninechars',) varargs = ('😑' * 5,) # 4 bytes * 5 - ret = xargs.partition(cmd, varargs, _max_length=30) + ret = xargs.partition(cmd, varargs, 1, _max_length=30) assert ret == (cmd + varargs,) def test_partition_limit_linux(linux_mock): cmd = ('ninechars',) varargs = ('😑' * 5,) - ret = xargs.partition(cmd, varargs, _max_length=30) + ret = xargs.partition(cmd, varargs, 1, _max_length=30) assert ret == (cmd + varargs,) @@ -90,12 +91,39 @@ def test_argument_too_long_with_large_unicode(linux_mock): cmd = ('ninechars',) varargs = ('😑' * 10,) # 4 bytes * 10 with pytest.raises(xargs.ArgumentTooLongError): - xargs.partition(cmd, varargs, _max_length=20) + xargs.partition(cmd, varargs, 1, _max_length=20) + + +def test_partition_target_concurrency(): + ret = xargs.partition( + ('foo',), ('A',) * 22, + 4, + _max_length=50, + ) + assert ret == ( + ('foo',) + ('A',) * 6, + ('foo',) + ('A',) * 6, + ('foo',) + ('A',) * 6, + ('foo',) + ('A',) * 4, + ) + + +def test_partition_target_concurrency_wont_make_tiny_partitions(): + ret = xargs.partition( + ('foo',), ('A',) * 10, + 4, + _max_length=50, + ) + assert ret == ( + ('foo',) + ('A',) * 4, + ('foo',) + ('A',) * 4, + ('foo',) + ('A',) * 2, + ) def test_argument_too_long(): with pytest.raises(xargs.ArgumentTooLongError): - xargs.partition(('a' * 5,), ('a' * 5,), _max_length=10) + xargs.partition(('a' * 5,), ('a' * 5,), 1, _max_length=10) def test_xargs_smoke(): From 231f6013bbadbf4c0e77f980ce359a4cd01063b2 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Mon, 22 Oct 2018 09:21:37 -0700 Subject: [PATCH 285/544] Allow more time on the concurrency test Spawning processes is apparently really slow on Windows, and the test is occasionally taking slightly more than a second on AppVeyor. I think we can allow up to the full 2.5 seconds without losing the valuable bits of the test. --- tests/xargs_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 3dcb6e8a1..da3cc74d6 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -177,4 +177,6 @@ def test_xargs_concurrency(): assert ret == 0 pids = stdout.splitlines() assert len(pids) == 5 - assert elapsed < 1 + # It would take 0.5*5=2.5 seconds ot run all of these in serial, so if it + # takes less, they must have run concurrently. + assert elapsed < 2.5 From aa50a8cde0919f0cf98b66b415403f04e54c7f05 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Mon, 22 Oct 2018 09:50:46 -0700 Subject: [PATCH 286/544] Switch to using concurrent.futures --- pre_commit/xargs.py | 49 +++++++++++++++++++++++---------------------- setup.py | 5 ++++- tests/xargs_test.py | 13 ++++++++++++ 3 files changed, 42 insertions(+), 25 deletions(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 9c4bc78ac..5222d5534 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -4,7 +4,6 @@ import contextlib import math -import multiprocessing.pool import sys import concurrent.futures @@ -79,12 +78,12 @@ def partition(cmd, varargs, target_concurrency, _max_length=None): @contextlib.contextmanager -def _threadpool(size): - pool = multiprocessing.pool.ThreadPool(size) - try: - yield pool - finally: - pool.terminate() +def _thread_mapper(maxsize): + if maxsize == 1: + yield map + else: + with concurrent.futures.ThreadPoolExecutor(maxsize) as ex: + yield ex.map def xargs(cmd, varargs, **kwargs): @@ -109,22 +108,24 @@ def xargs(cmd, varargs, **kwargs): def run_cmd_partition(run_cmd): return cmd_output(*run_cmd, encoding=None, retcode=None) - with _threadpool(min(len(partitions), target_concurrency)) as pool: - results = pool.map(run_cmd_partition, partitions) - - for proc_retcode, proc_out, proc_err in results: - # This is *slightly* too clever so I'll explain it. - # First the xor boolean table: - # T | F | - # +-------+ - # T | F | T | - # --+-------+ - # F | T | F | - # --+-------+ - # When negate is True, it has the effect of flipping the return code - # Otherwise, the retuncode is unchanged - retcode |= bool(proc_retcode) ^ negate - stdout += proc_out - stderr += proc_err + with _thread_mapper( + min(len(partitions), target_concurrency), + ) as thread_map: + results = thread_map(run_cmd_partition, partitions) + + for proc_retcode, proc_out, proc_err in results: + # This is *slightly* too clever so I'll explain it. + # First the xor boolean table: + # T | F | + # +-------+ + # T | F | T | + # --+-------+ + # F | T | F | + # --+-------+ + # When negate is True, it has the effect of flipping the return + # code. Otherwise, the returncode is unchanged. + retcode |= bool(proc_retcode) ^ negate + stdout += proc_out + stderr += proc_err return retcode, stdout, stderr diff --git a/setup.py b/setup.py index 7c0a958f4..dd3eb4252 100644 --- a/setup.py +++ b/setup.py @@ -47,7 +47,10 @@ 'toml', 'virtualenv', ], - extras_require={':python_version<"3.7"': ['importlib-resources']}, + extras_require={ + ':python_version<"3.2"': ['futures'], + ':python_version<"3.7"': ['importlib-resources'], + }, entry_points={ 'console_scripts': [ 'pre-commit = pre_commit.main:main', diff --git a/tests/xargs_test.py b/tests/xargs_test.py index da3cc74d6..ed65ed462 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -5,6 +5,7 @@ import sys import time +import concurrent.futures import mock import pytest import six @@ -180,3 +181,15 @@ def test_xargs_concurrency(): # It would take 0.5*5=2.5 seconds ot run all of these in serial, so if it # takes less, they must have run concurrently. assert elapsed < 2.5 + + +def test_thread_mapper_concurrency_uses_threadpoolexecutor_map(): + with xargs._thread_mapper(10) as thread_map: + assert isinstance( + thread_map.__self__, concurrent.futures.ThreadPoolExecutor, + ) is True + + +def test_thread_mapper_concurrency_uses_regular_map(): + with xargs._thread_mapper(1) as thread_map: + assert thread_map is map From 9125439c3a6b7549bcf6d82c36fc2b89d1283cb2 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Mon, 22 Oct 2018 09:51:14 -0700 Subject: [PATCH 287/544] Force serial hook runs during tests --- pre_commit/languages/helpers.py | 2 +- tests/languages/helpers_test.py | 13 +++++++++++-- tox.ini | 1 + 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index abd28fa07..8b3e590d5 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -50,7 +50,7 @@ def no_install(prefix, version, additional_dependencies): def target_concurrency(hook): - if hook['require_serial']: + if hook['require_serial'] or 'PRE_COMMIT_NO_CONCURRENCY' in os.environ: return 1 else: # Travis appears to have a bunch of CPUs, but we can't use them all. diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index f1c1497f7..e7bd47027 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -40,7 +40,15 @@ def test_target_concurrency_normal(): def test_target_concurrency_cpu_count_require_serial_true(): - assert helpers.target_concurrency({'require_serial': True}) == 1 + with mock.patch.dict(os.environ, {}, clear=True): + assert helpers.target_concurrency({'require_serial': True}) == 1 + + +def test_target_concurrency_testing_env_var(): + with mock.patch.dict( + os.environ, {'PRE_COMMIT_NO_CONCURRENCY': '1'}, clear=True, + ): + assert helpers.target_concurrency({'require_serial': False}) == 1 def test_target_concurrency_on_travis(): @@ -52,4 +60,5 @@ def test_target_concurrency_cpu_count_not_implemented(): with mock.patch.object( multiprocessing, 'cpu_count', side_effect=NotImplementedError, ): - assert helpers.target_concurrency({'require_serial': False}) == 1 + with mock.patch.dict(os.environ, {}, clear=True): + assert helpers.target_concurrency({'require_serial': False}) == 1 diff --git a/tox.ini b/tox.ini index d4b590bf0..52f3d3ee1 100644 --- a/tox.ini +++ b/tox.ini @@ -27,3 +27,4 @@ env = GIT_AUTHOR_EMAIL=test@example.com GIT_COMMITTER_EMAIL=test@example.com VIRTUALENV_NO_DOWNLOAD=1 + PRE_COMMIT_NO_CONCURRENCY=1 From 1c97d3f5fde3804ded59f65ef8f12ea429638c4d Mon Sep 17 00:00:00 2001 From: Milos Pejanovic Date: Wed, 31 Oct 2018 17:39:47 +0100 Subject: [PATCH 288/544] Added a try except block which reraises InvalidManifestError as RepositoryCannotBeUpdatedError --- pre_commit/commands/autoupdate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 8f3714c49..d08ea411b 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -14,6 +14,7 @@ from pre_commit.clientlib import is_local_repo from pre_commit.clientlib import is_meta_repo from pre_commit.clientlib import load_config +from pre_commit.clientlib import InvalidManifestError from pre_commit.commands.migrate_config import migrate_config from pre_commit.repository import Repository from pre_commit.util import CalledProcessError @@ -57,7 +58,10 @@ def _update_repo(repo_config, store, tags_only): # See if any of our hooks were deleted with the new commits hooks = {hook['id'] for hook in repo_config['hooks']} - hooks_missing = hooks - (hooks & set(new_repo.manifest_hooks)) + try: + hooks_missing = hooks - (hooks & set(new_repo.manifest_hooks)) + except InvalidManifestError as e: + raise RepositoryCannotBeUpdatedError(e.args[0]) if hooks_missing: raise RepositoryCannotBeUpdatedError( 'Cannot update because the tip of master is missing these hooks:\n' From 6bac405d40b25409cbfb36cfedf4d6113ad19014 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 1 Nov 2018 18:05:36 -0700 Subject: [PATCH 289/544] Minor cleanups --- pre_commit/languages/docker.py | 7 +------ pre_commit/languages/docker_image.py | 7 +------ pre_commit/languages/golang.py | 7 +------ pre_commit/languages/helpers.py | 5 +++++ pre_commit/languages/node.py | 7 +------ pre_commit/languages/python.py | 7 +------ pre_commit/languages/ruby.py | 7 +------ pre_commit/languages/rust.py | 7 +------ pre_commit/languages/script.py | 7 +------ pre_commit/languages/swift.py | 7 +------ pre_commit/languages/system.py | 7 +------ pre_commit/xargs.py | 5 ++--- 12 files changed, 17 insertions(+), 63 deletions(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 7f00fe60e..bfdd35854 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -9,7 +9,6 @@ from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output -from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'docker' @@ -97,8 +96,4 @@ def run_hook(prefix, hook, file_args): # pragma: windows no cover entry_tag = ('--entrypoint', entry_exe, docker_tag(prefix)) cmd = docker_cmd() + entry_tag + cmd_rest - return xargs( - cmd, - file_args, - target_concurrency=helpers.target_concurrency(hook), - ) + return helpers.run_xargs(hook, cmd, file_args) diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index e990f18aa..e7ebad7f0 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -4,7 +4,6 @@ from pre_commit.languages import helpers from pre_commit.languages.docker import assert_docker_available from pre_commit.languages.docker import docker_cmd -from pre_commit.xargs import xargs ENVIRONMENT_DIR = None @@ -16,8 +15,4 @@ def run_hook(prefix, hook, file_args): # pragma: windows no cover assert_docker_available() cmd = docker_cmd() + helpers.to_cmd(hook) - return xargs( - cmd, - file_args, - target_concurrency=helpers.target_concurrency(hook), - ) + return helpers.run_xargs(hook, cmd, file_args) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 7d273e752..09e3476c5 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -11,7 +11,6 @@ from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import rmtree -from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'golangenv' @@ -81,8 +80,4 @@ def install_environment(prefix, version, additional_dependencies): def run_hook(prefix, hook, file_args): with in_env(prefix): - return xargs( - helpers.to_cmd(hook), - file_args, - target_concurrency=helpers.target_concurrency(hook), - ) + return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 8b3e590d5..aa5a5d13e 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -5,6 +5,7 @@ import shlex from pre_commit.util import cmd_output +from pre_commit.xargs import xargs def run_setup_cmd(prefix, cmd): @@ -61,3 +62,7 @@ def target_concurrency(hook): return multiprocessing.cpu_count() except NotImplementedError: return 1 + + +def run_xargs(hook, cmd, file_args): + return xargs(cmd, file_args, target_concurrency=target_concurrency(hook)) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 494ca878e..8e5dc7e5c 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -10,7 +10,6 @@ from pre_commit.languages.python import bin_dir from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output -from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'node_env' @@ -71,8 +70,4 @@ def install_environment(prefix, version, additional_dependencies): def run_hook(prefix, hook, file_args): with in_env(prefix, hook['language_version']): - return xargs( - helpers.to_cmd(hook), - file_args, - target_concurrency=helpers.target_concurrency(hook), - ) + return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index bb8a81a66..4b7580a4e 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -12,7 +12,6 @@ from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output -from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'py_env' @@ -127,11 +126,7 @@ def healthy(prefix, language_version): def run_hook(prefix, hook, file_args): with in_env(prefix, hook['language_version']): - return xargs( - helpers.to_cmd(hook), - file_args, - target_concurrency=helpers.target_concurrency(hook), - ) + return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) def install_environment(prefix, version, additional_dependencies): additional_dependencies = tuple(additional_dependencies) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 3c5745df1..0330ae8d6 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -12,7 +12,6 @@ from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import resource_bytesio -from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'rbenv' @@ -126,8 +125,4 @@ def install_environment( def run_hook(prefix, hook, file_args): # pragma: windows no cover with in_env(prefix, hook['language_version']): - return xargs( - helpers.to_cmd(hook), - file_args, - target_concurrency=helpers.target_concurrency(hook), - ) + return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index e602adcc6..8a5a07048 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -10,7 +10,6 @@ from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output -from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'rustenv' @@ -91,8 +90,4 @@ def install_environment(prefix, version, additional_dependencies): def run_hook(prefix, hook, file_args): with in_env(prefix): - return xargs( - helpers.to_cmd(hook), - file_args, - target_concurrency=helpers.target_concurrency(hook), - ) + return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index d242694f5..809efb854 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals from pre_commit.languages import helpers -from pre_commit.xargs import xargs ENVIRONMENT_DIR = None @@ -13,8 +12,4 @@ def run_hook(prefix, hook, file_args): cmd = helpers.to_cmd(hook) cmd = (prefix.path(cmd[0]),) + cmd[1:] - return xargs( - cmd, - file_args, - target_concurrency=helpers.target_concurrency(hook), - ) + return helpers.run_xargs(hook, cmd, file_args) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index eff4f9b07..c282de5d9 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -8,7 +8,6 @@ from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output -from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'swift_env' get_default_version = helpers.basic_get_default_version @@ -53,8 +52,4 @@ def install_environment( def run_hook(prefix, hook, file_args): # pragma: windows no cover with in_env(prefix): - return xargs( - helpers.to_cmd(hook), - file_args, - target_concurrency=helpers.target_concurrency(hook), - ) + return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 70a42ddcd..e590d4868 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals from pre_commit.languages import helpers -from pre_commit.xargs import xargs ENVIRONMENT_DIR = None @@ -11,8 +10,4 @@ def run_hook(prefix, hook, file_args): - return xargs( - helpers.to_cmd(hook), - file_args, - target_concurrency=helpers.target_concurrency(hook), - ) + return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 5222d5534..3b4a25f9c 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -108,9 +108,8 @@ def xargs(cmd, varargs, **kwargs): def run_cmd_partition(run_cmd): return cmd_output(*run_cmd, encoding=None, retcode=None) - with _thread_mapper( - min(len(partitions), target_concurrency), - ) as thread_map: + threads = min(len(partitions), target_concurrency) + with _thread_mapper(threads) as thread_map: results = thread_map(run_cmd_partition, partitions) for proc_retcode, proc_out, proc_err in results: From bf8c8521cdf26006eff64bb2d4a35b77dcb0667a Mon Sep 17 00:00:00 2001 From: Milos Pejanovic Date: Wed, 14 Nov 2018 00:43:04 +0100 Subject: [PATCH 290/544] Added a test and small change for error output --- pre_commit/commands/autoupdate.py | 2 +- tests/commands/autoupdate_test.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index d08ea411b..a02efe089 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -132,7 +132,7 @@ def autoupdate(runner, store, tags_only, repos=()): try: new_repo_config = _update_repo(repo_config, store, tags_only) except RepositoryCannotBeUpdatedError as error: - output.write_line(error.args[0]) + output.write_line(str(error)) output_repos.append(repo_config) retv = 1 continue diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 3bfb62e0e..b6e81b2a0 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -260,6 +260,21 @@ def test_autoupdate_tags_only(tagged_repo_with_more_commits, in_tmpdir, store): assert 'v1.2.3' in f.read() +def test_autoupdate_latest_no_config(out_of_date_repo, in_tmpdir, store): + config = make_config_from_repo( + out_of_date_repo.path, rev=out_of_date_repo.original_rev, + ) + write_config('.', config) + + cmd_output('git', '-C', out_of_date_repo.path, 'rm', '-r', ':/') + cmd_output('git', '-C', out_of_date_repo.path, 'commit', '-m', 'rm') + + ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) + assert ret == 1 + with open(C.CONFIG_FILE) as f: + assert out_of_date_repo.original_rev in f.read() + + @pytest.fixture def hook_disappearing_repo(tempdir_factory): path = make_repo(tempdir_factory, 'python_hooks_repo') From e339de22d76b714130d797a80097e9ef13ef0543 Mon Sep 17 00:00:00 2001 From: Milos Pejanovic Date: Wed, 14 Nov 2018 01:59:18 +0100 Subject: [PATCH 291/544] Added requested changes --- pre_commit/commands/autoupdate.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index a02efe089..0bff116c1 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -11,10 +11,10 @@ import pre_commit.constants as C from pre_commit import output from pre_commit.clientlib import CONFIG_SCHEMA +from pre_commit.clientlib import InvalidManifestError from pre_commit.clientlib import is_local_repo from pre_commit.clientlib import is_meta_repo from pre_commit.clientlib import load_config -from pre_commit.clientlib import InvalidManifestError from pre_commit.commands.migrate_config import migrate_config from pre_commit.repository import Repository from pre_commit.util import CalledProcessError @@ -54,14 +54,15 @@ def _update_repo(repo_config, store, tags_only): # Construct a new config with the head rev new_config = OrderedDict(repo_config) new_config['rev'] = rev - new_repo = Repository.create(new_config, store) - # See if any of our hooks were deleted with the new commits - hooks = {hook['id'] for hook in repo_config['hooks']} try: - hooks_missing = hooks - (hooks & set(new_repo.manifest_hooks)) + new_hooks = Repository.create(new_config, store).manifest_hooks except InvalidManifestError as e: raise RepositoryCannotBeUpdatedError(e.args[0]) + + # See if any of our hooks were deleted with the new commits + hooks = {hook['id'] for hook in repo_config['hooks']} + hooks_missing = hooks - set(new_hooks) if hooks_missing: raise RepositoryCannotBeUpdatedError( 'Cannot update because the tip of master is missing these hooks:\n' From aaa3976a29c1e4099029adaabebe2b076a3ad052 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 13 Nov 2018 17:23:32 -0800 Subject: [PATCH 292/544] Use text_type instead of str() --- pre_commit/commands/autoupdate.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 0bff116c1..d93d7e11e 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -4,6 +4,7 @@ import re from collections import OrderedDict +import six from aspy.yaml import ordered_dump from aspy.yaml import ordered_load from cfgv import remove_defaults @@ -58,7 +59,7 @@ def _update_repo(repo_config, store, tags_only): try: new_hooks = Repository.create(new_config, store).manifest_hooks except InvalidManifestError as e: - raise RepositoryCannotBeUpdatedError(e.args[0]) + raise RepositoryCannotBeUpdatedError(six.text_type(e)) # See if any of our hooks were deleted with the new commits hooks = {hook['id'] for hook in repo_config['hooks']} @@ -133,7 +134,7 @@ def autoupdate(runner, store, tags_only, repos=()): try: new_repo_config = _update_repo(repo_config, store, tags_only) except RepositoryCannotBeUpdatedError as error: - output.write_line(str(error)) + output.write_line(error.args[0]) output_repos.append(repo_config) retv = 1 continue From e15d7cde86e527f831ae54b8ef3014976681b047 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 15 Nov 2018 14:17:10 -0800 Subject: [PATCH 293/544] Upgrade the sample config --- pre_commit/commands/sample_config.py | 2 +- tests/commands/sample_config_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/sample_config.py b/pre_commit/commands/sample_config.py index 87bcaa7d9..38320f67b 100644 --- a/pre_commit/commands/sample_config.py +++ b/pre_commit/commands/sample_config.py @@ -12,7 +12,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v1.4.0 + rev: v2.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/tests/commands/sample_config_test.py b/tests/commands/sample_config_test.py index cd43d45f2..83942a4f0 100644 --- a/tests/commands/sample_config_test.py +++ b/tests/commands/sample_config_test.py @@ -13,7 +13,7 @@ def test_sample_config(capsys): # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v1.4.0 + rev: v2.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer From 45e3dab00ddbd1763543438381ebf184f19319c9 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Mon, 19 Nov 2018 17:36:57 -0800 Subject: [PATCH 294/544] Shuffle arguments before running hooks --- pre_commit/languages/helpers.py | 22 ++++++++++++++++++++++ tests/languages/helpers_test.py | 4 ++++ 2 files changed, 26 insertions(+) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index aa5a5d13e..7ab117bf1 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -2,12 +2,18 @@ import multiprocessing import os +import random import shlex +import six + from pre_commit.util import cmd_output from pre_commit.xargs import xargs +FIXED_RANDOM_SEED = 1542676186 + + def run_setup_cmd(prefix, cmd): cmd_output(*cmd, cwd=prefix.prefix_dir, encoding=None) @@ -64,5 +70,21 @@ def target_concurrency(hook): return 1 +def _shuffled(seq): + """Deterministically shuffle identically under both py2 + py3.""" + fixed_random = random.Random() + if six.PY2: # pragma: no cover (py2) + fixed_random.seed(FIXED_RANDOM_SEED) + else: + fixed_random.seed(FIXED_RANDOM_SEED, version=1) + + seq = list(seq) + random.shuffle(seq, random=fixed_random.random) + return seq + + def run_xargs(hook, cmd, file_args): + # Shuffle the files so that they more evenly fill out the xargs partitions, + # but do it deterministically in case a hook cares about ordering. + file_args = _shuffled(file_args) return xargs(cmd, file_args, target_concurrency=target_concurrency(hook)) diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index e7bd47027..f77c3053c 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -62,3 +62,7 @@ def test_target_concurrency_cpu_count_not_implemented(): ): with mock.patch.dict(os.environ, {}, clear=True): assert helpers.target_concurrency({'require_serial': False}) == 1 + + +def test_shuffled_is_deterministic(): + assert helpers._shuffled(range(10)) == [3, 7, 8, 2, 4, 6, 5, 1, 0, 9] From afeac2f099927e90db42c154613ca0ea8b1927f0 Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Tue, 11 Dec 2018 12:32:40 +0000 Subject: [PATCH 295/544] Don't fail if GPG signing is configured by default --- testing/fixtures.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/testing/fixtures.py b/testing/fixtures.py index 2b2e280e3..e78856324 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -48,7 +48,7 @@ def make_repo(tempdir_factory, repo_source): path = git_dir(tempdir_factory) copy_tree_to_path(get_resource_path(repo_source), path) cmd_output('git', 'add', '.', cwd=path) - cmd_output('git', 'commit', '-m', 'Add hooks', cwd=path) + cmd_output('git', 'commit', '--no-gpg-sign', '-m', 'Add hooks', cwd=path) return path @@ -64,7 +64,8 @@ def modify_manifest(path): with io.open(manifest_path, 'w') as manifest_file: manifest_file.write(ordered_dump(manifest, **C.YAML_DUMP_KWARGS)) cmd_output( - 'git', 'commit', '-am', 'update {}'.format(C.MANIFEST_FILE), cwd=path, + 'git', 'commit', '--no-gpg-sign', '-am', + 'update {}'.format(C.MANIFEST_FILE), cwd=path, ) @@ -80,7 +81,9 @@ def modify_config(path='.', commit=True): with io.open(config_path, 'w', encoding='UTF-8') as config_file: config_file.write(ordered_dump(config, **C.YAML_DUMP_KWARGS)) if commit: - cmd_output('git', 'commit', '-am', 'update config', cwd=path) + cmd_output( + 'git', 'commit', '--no-gpg-sign', '-am', 'update config', cwd=path, + ) def config_with_local_hooks(): @@ -136,13 +139,19 @@ def write_config(directory, config, config_file=C.CONFIG_FILE): def add_config_to_repo(git_path, config, config_file=C.CONFIG_FILE): write_config(git_path, config, config_file=config_file) cmd_output('git', 'add', config_file, cwd=git_path) - cmd_output('git', 'commit', '-m', 'Add hooks config', cwd=git_path) + cmd_output( + 'git', 'commit', '--no-gpg-sign', '-m', 'Add hooks config', + cwd=git_path, + ) return git_path def remove_config_from_repo(git_path, config_file=C.CONFIG_FILE): cmd_output('git', 'rm', config_file, cwd=git_path) - cmd_output('git', 'commit', '-m', 'Remove hooks config', cwd=git_path) + cmd_output( + 'git', 'commit', '--no-gpg-sign', '-m', 'Remove hooks config', + cwd=git_path, + ) return git_path From 15b1f118b5a4c97f5a804a053037de3f9f7d945e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 14 Dec 2018 13:14:13 -0800 Subject: [PATCH 296/544] Update fixtures.py --- testing/fixtures.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testing/fixtures.py b/testing/fixtures.py index e78856324..287eb3092 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -65,7 +65,8 @@ def modify_manifest(path): manifest_file.write(ordered_dump(manifest, **C.YAML_DUMP_KWARGS)) cmd_output( 'git', 'commit', '--no-gpg-sign', '-am', - 'update {}'.format(C.MANIFEST_FILE), cwd=path, + 'update {}'.format(C.MANIFEST_FILE), + cwd=path, ) From 435d9945a34ceedc97d5219b2bbb7cc88c9ac8e8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Dec 2018 14:22:09 -0800 Subject: [PATCH 297/544] Switch from deprecated docs-off args to --no-document --- pre_commit/languages/ruby.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 0330ae8d6..7bd14f19a 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -118,7 +118,7 @@ def install_environment( ) helpers.run_setup_cmd( prefix, - ('gem', 'install', '--no-ri', '--no-rdoc') + + ('gem', 'install', '--no-document') + prefix.star('.gem') + additional_dependencies, ) From 91782bb6c85d15d1718d7e154bfd12a9ebe9f289 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Dec 2018 14:08:20 -0800 Subject: [PATCH 298/544] xfail windows node until #887 is resolved --- testing/util.py | 1 + 1 file changed, 1 insertion(+) diff --git a/testing/util.py b/testing/util.py index 6a66c7c9a..ed38affeb 100644 --- a/testing/util.py +++ b/testing/util.py @@ -48,6 +48,7 @@ def cmd_output_mocked_pre_commit_home(*args, **kwargs): def broken_deep_listdir(): # pragma: no cover (platform specific) if sys.platform != 'win32': return False + return True # TODO: remove this after #887 is resolved try: os.listdir(str('\\\\?\\') + os.path.abspath(str('.'))) except OSError: From 748c2ad273a61bbedb8b92cf85889f91ddc33c67 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 20 Dec 2018 12:05:22 -0800 Subject: [PATCH 299/544] v1.13.0 --- CHANGELOG.md | 38 ++++++++++++++++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 355b08248..9f8fc775b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,41 @@ +1.13.0 +====== + +### Features +- Run hooks in parallel + - individual hooks may opt out of parallel exection with `parallel: false` + - #510 issue by @chriskuehl. + - #851 PR by @chriskuehl. + +### Fixes +- Improve platform-specific `xargs` command length detection + - #691 issue by @antonbabenko. + - #839 PR by @georgeyk. +- Fix `pre-commit autoupdate` when updating to a latest tag missing a + `.pre-commit-hooks.yaml` + - #856 issue by @asottile. + - #857 PR by @runz0rd. +- Upgrade the `pre-commit-hooks` version in `pre-commit sample-config` + - #870 by @asottile. +- Improve balancing of multiprocessing by deterministic shuffling of args + - #861 issue by @Dunedan. + - #874 PR by @chriskuehl. +- `ruby` hooks work with latest `gem` by removing `--no-ri` / `--no-rdoc` and + instead using `--no-document`. + - #889 PR by @asottile. + +### Misc +- Use `--no-gpg-sign` when running tests + - #885 PR by @s0undt3ch. + +### Updating +- If a hook requires serial execution, set `parallel: false` to avoid the new + parallel execution. +- `ruby` hooks now require `gem>=2.0.0`. If your platform doesn't support this + by default, select a newer version using + [`language_version`](https://pre-commit.com/#overriding-language-version). + + 1.12.0 ====== diff --git a/setup.py b/setup.py index dd3eb4252..edcd04ff8 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.12.0', + version='1.13.0', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From de942894ffad9eb3a117b4ba8d4abe2c17f98074 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 25 Dec 2018 12:11:02 -0800 Subject: [PATCH 300/544] Pick a better python shebang for hook executable --- pre_commit/commands/install_uninstall.py | 13 +++++++++++++ tests/commands/install_uninstall_test.py | 20 ++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index d31330603..6bd4602b7 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -8,6 +8,7 @@ from pre_commit import git from pre_commit import output +from pre_commit.languages import python from pre_commit.repository import repositories from pre_commit.util import cmd_output from pre_commit.util import make_executable @@ -43,6 +44,16 @@ def is_our_script(filename): return any(h in contents for h in (CURRENT_HASH,) + PRIOR_HASHES) +def shebang(): + if sys.platform == 'win32': + py = 'python' + else: + py = python.get_default_version() + if py == 'default': + py = 'python' + return '#!/usr/bin/env {}'.format(py) + + def install( runner, store, overwrite=False, hooks=False, hook_type='pre-commit', skip_on_missing_conf=False, @@ -84,6 +95,8 @@ def install( before, rest = contents.split(TEMPLATE_START) to_template, after = rest.split(TEMPLATE_END) + before = before.replace('#!/usr/bin/env python', shebang()) + hook_file.write(before + TEMPLATE_START) for line in to_template.splitlines(): var = line.split()[0] diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index fce0010be..dbf663e97 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -17,7 +17,9 @@ from pre_commit.commands.install_uninstall import install_hooks from pre_commit.commands.install_uninstall import is_our_script from pre_commit.commands.install_uninstall import PRIOR_HASHES +from pre_commit.commands.install_uninstall import shebang from pre_commit.commands.install_uninstall import uninstall +from pre_commit.languages import python from pre_commit.runner import Runner from pre_commit.util import cmd_output from pre_commit.util import make_executable @@ -45,6 +47,24 @@ def test_is_previous_pre_commit(tmpdir): assert is_our_script(f.strpath) +def test_shebang_windows(): + with mock.patch.object(sys, 'platform', 'win32'): + assert shebang() == '#!/usr/bin/env python' + + +def test_shebang_otherwise(): + with mock.patch.object(sys, 'platform', 'posix'): + assert 'default' not in shebang() + + +def test_shebang_returns_default(): + with mock.patch.object(sys, 'platform', 'posix'): + with mock.patch.object( + python, 'get_default_version', return_value='default', + ): + assert shebang() == '#!/usr/bin/env python' + + def test_install_pre_commit(tempdir_factory, store): path = git_dir(tempdir_factory) runner = Runner(path, C.CONFIG_FILE) From fe409f1a436cbe3bc8220ec65b3c8a658f541a18 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 26 Dec 2018 22:33:21 -0800 Subject: [PATCH 301/544] Remove stateful Runner --- pre_commit/commands/autoupdate.py | 8 +- pre_commit/commands/install_uninstall.py | 22 +-- pre_commit/commands/migrate_config.py | 6 +- pre_commit/commands/run.py | 16 ++- pre_commit/commands/try_repo.py | 3 +- pre_commit/git.py | 2 +- pre_commit/languages/ruby.py | 6 +- pre_commit/main.py | 36 +++-- pre_commit/runner.py | 36 ----- tests/commands/autoupdate_test.py | 49 +++---- tests/commands/install_uninstall_test.py | 172 +++++++++-------------- tests/commands/migrate_config_test.py | 16 ++- tests/commands/run_test.py | 10 +- tests/conftest.py | 7 + tests/main_test.py | 84 ++++++----- tests/runner_test.py | 44 ------ tests/staged_files_only_test.py | 7 - 17 files changed, 209 insertions(+), 315 deletions(-) delete mode 100644 pre_commit/runner.py delete mode 100644 tests/runner_test.py diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index d93d7e11e..f40a7c555 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -112,14 +112,14 @@ def _write_new_config_file(path, output): f.write(to_write) -def autoupdate(runner, store, tags_only, repos=()): +def autoupdate(config_file, store, tags_only, repos=()): """Auto-update the pre-commit config to the latest versions of repos.""" - migrate_config(runner, quiet=True) + migrate_config(config_file, quiet=True) retv = 0 output_repos = [] changed = False - input_config = load_config(runner.config_file_path) + input_config = load_config(config_file) for repo_config in input_config['repos']: if ( @@ -152,6 +152,6 @@ def autoupdate(runner, store, tags_only, repos=()): if changed: output_config = input_config.copy() output_config['repos'] = output_repos - _write_new_config_file(runner.config_file_path, output_config) + _write_new_config_file(config_file, output_config) return retv diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 6bd4602b7..3e70b4c9f 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -8,6 +8,7 @@ from pre_commit import git from pre_commit import output +from pre_commit.clientlib import load_config from pre_commit.languages import python from pre_commit.repository import repositories from pre_commit.util import cmd_output @@ -31,8 +32,8 @@ TEMPLATE_END = '# end templated\n' -def _hook_paths(git_root, hook_type): - pth = os.path.join(git.get_git_dir(git_root), 'hooks', hook_type) +def _hook_paths(hook_type): + pth = os.path.join(git.get_git_dir(), 'hooks', hook_type) return pth, '{}.legacy'.format(pth) @@ -55,7 +56,8 @@ def shebang(): def install( - runner, store, overwrite=False, hooks=False, hook_type='pre-commit', + config_file, store, + overwrite=False, hooks=False, hook_type='pre-commit', skip_on_missing_conf=False, ): """Install the pre-commit hooks.""" @@ -66,7 +68,7 @@ def install( ) return 1 - hook_path, legacy_path = _hook_paths(runner.git_root, hook_type) + hook_path, legacy_path = _hook_paths(hook_type) mkdirp(os.path.dirname(hook_path)) @@ -84,7 +86,7 @@ def install( ) params = { - 'CONFIG': runner.config_file, + 'CONFIG': config_file, 'HOOK_TYPE': hook_type, 'INSTALL_PYTHON': sys.executable, 'SKIP_ON_MISSING_CONFIG': skip_on_missing_conf, @@ -108,19 +110,19 @@ def install( # If they requested we install all of the hooks, do so. if hooks: - install_hooks(runner, store) + install_hooks(config_file, store) return 0 -def install_hooks(runner, store): - for repository in repositories(runner.config, store): +def install_hooks(config_file, store): + for repository in repositories(load_config(config_file), store): repository.require_installed() -def uninstall(runner, hook_type='pre-commit'): +def uninstall(hook_type='pre-commit'): """Uninstall the pre-commit hooks.""" - hook_path, legacy_path = _hook_paths(runner.git_root, hook_type) + hook_path, legacy_path = _hook_paths(hook_type) # If our file doesn't exist or it isn't ours, gtfo. if not os.path.exists(hook_path) or not is_our_script(hook_path): diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index b43367fb9..3f73bb83e 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -45,15 +45,15 @@ def _migrate_sha_to_rev(contents): return reg.sub(r'\1rev:', contents) -def migrate_config(runner, quiet=False): - with io.open(runner.config_file_path) as f: +def migrate_config(config_file, quiet=False): + with io.open(config_file) as f: orig_contents = contents = f.read() contents = _migrate_map(contents) contents = _migrate_sha_to_rev(contents) if contents != orig_contents: - with io.open(runner.config_file_path, 'w') as f: + with io.open(config_file, 'w') as f: f.write(contents) print('Configuration has been migrated.') diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index dbf564102..f2ff7b38c 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -11,6 +11,7 @@ from pre_commit import color from pre_commit import git from pre_commit import output +from pre_commit.clientlib import load_config from pre_commit.output import get_hook_message from pre_commit.repository import repositories from pre_commit.staged_files_only import staged_files_only @@ -214,16 +215,16 @@ def _has_unmerged_paths(): return bool(stdout.strip()) -def _has_unstaged_config(runner): +def _has_unstaged_config(config_file): retcode, _, _ = cmd_output( - 'git', 'diff', '--no-ext-diff', '--exit-code', runner.config_file_path, + 'git', 'diff', '--no-ext-diff', '--exit-code', config_file, retcode=None, ) # be explicit, other git errors don't mean it has an unstaged config. return retcode == 1 -def run(runner, store, args, environ=os.environ): +def run(config_file, store, args, environ=os.environ): no_stash = args.all_files or bool(args.files) # Check if we have unresolved merge conflict files and fail fast. @@ -233,10 +234,10 @@ def run(runner, store, args, environ=os.environ): if bool(args.source) != bool(args.origin): logger.error('Specify both --origin and --source.') return 1 - if _has_unstaged_config(runner) and not no_stash: + if _has_unstaged_config(config_file) and not no_stash: logger.error( 'Your pre-commit configuration is unstaged.\n' - '`git add {}` to fix this.'.format(runner.config_file), + '`git add {}` to fix this.'.format(config_file), ) return 1 @@ -252,7 +253,8 @@ def run(runner, store, args, environ=os.environ): with ctx: repo_hooks = [] - for repo in repositories(runner.config, store): + config = load_config(config_file) + for repo in repositories(config, store): for _, hook in repo.hooks: if ( (not args.hook or hook['id'] == args.hook) and @@ -267,4 +269,4 @@ def run(runner, store, args, environ=os.environ): for repo in {repo for repo, _ in repo_hooks}: repo.require_installed() - return _run_hooks(runner.config, repo_hooks, args, environ) + return _run_hooks(config, repo_hooks, args, environ) diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index 431db1413..e964987c7 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -11,7 +11,6 @@ from pre_commit import output from pre_commit.clientlib import load_manifest from pre_commit.commands.run import run -from pre_commit.runner import Runner from pre_commit.store import Store from pre_commit.util import tmpdir @@ -43,4 +42,4 @@ def try_repo(args): output.write(config_s) output.write_line('=' * 79) - return run(Runner('.', config_filename), store, args) + return run(config_filename, store, args) diff --git a/pre_commit/git.py b/pre_commit/git.py index a92611632..84db66ea4 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -30,7 +30,7 @@ def get_root(): ) -def get_git_dir(git_root): +def get_git_dir(git_root='.'): opts = ('--git-common-dir', '--git-dir') _, out, _ = cmd_output('git', 'rev-parse', *opts, cwd=git_root) for line, opt in zip(out.splitlines(), opts): diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 7bd14f19a..484df47c7 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -88,12 +88,12 @@ def _install_rbenv(prefix, version='default'): # pragma: windows no cover activate_file.write('export RBENV_VERSION="{}"\n'.format(version)) -def _install_ruby(runner, version): # pragma: windows no cover +def _install_ruby(prefix, version): # pragma: windows no cover try: - helpers.run_setup_cmd(runner, ('rbenv', 'download', version)) + helpers.run_setup_cmd(prefix, ('rbenv', 'download', version)) except CalledProcessError: # pragma: no cover (usually find with download) # Failed to download from mirror for some reason, build it instead - helpers.run_setup_cmd(runner, ('rbenv', 'install', version)) + helpers.run_setup_cmd(prefix, ('rbenv', 'install', version)) def install_environment( diff --git a/pre_commit/main.py b/pre_commit/main.py index fafe36b12..a5a4a817d 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -20,7 +20,6 @@ from pre_commit.commands.try_repo import try_repo from pre_commit.error_handler import error_handler from pre_commit.logging_handler import add_logging_handler -from pre_commit.runner import Runner from pre_commit.store import Store @@ -89,6 +88,20 @@ def _add_run_options(parser): ) +def _adjust_args_and_chdir(args): + # `--config` was specified relative to the non-root working directory + if os.path.exists(args.config): + args.config = os.path.abspath(args.config) + if args.command in {'run', 'try-repo'}: + args.files = [os.path.abspath(filename) for filename in args.files] + + os.chdir(git.get_root()) + + args.config = os.path.relpath(args.config) + if args.command in {'run', 'try-repo'}: + args.files = [os.path.relpath(filename) for filename in args.files] + + def main(argv=None): argv = argv if argv is not None else sys.argv[1:] argv = [five.to_text(arg) for arg in argv] @@ -222,43 +235,40 @@ def main(argv=None): parser.parse_args([args.help_cmd, '--help']) elif args.command == 'help': parser.parse_args(['--help']) - elif args.command in {'run', 'try-repo'}: - args.files = [ - os.path.relpath(os.path.abspath(filename), git.get_root()) - for filename in args.files - ] with error_handler(): add_logging_handler(args.color) - runner = Runner.create(args.config) + + _adjust_args_and_chdir(args) + store = Store() git.check_for_cygwin_mismatch() if args.command == 'install': return install( - runner, store, + args.config, store, overwrite=args.overwrite, hooks=args.install_hooks, hook_type=args.hook_type, skip_on_missing_conf=args.allow_missing_config, ) elif args.command == 'install-hooks': - return install_hooks(runner, store) + return install_hooks(args.config, store) elif args.command == 'uninstall': - return uninstall(runner, hook_type=args.hook_type) + return uninstall(hook_type=args.hook_type) elif args.command == 'clean': return clean(store) elif args.command == 'autoupdate': if args.tags_only: logger.warning('--tags-only is the default') return autoupdate( - runner, store, + args.config, store, tags_only=not args.bleeding_edge, repos=args.repos, ) elif args.command == 'migrate-config': - return migrate_config(runner) + return migrate_config(args.config) elif args.command == 'run': - return run(runner, store, args) + return run(args.config, store, args) elif args.command == 'sample-config': return sample_config() elif args.command == 'try-repo': diff --git a/pre_commit/runner.py b/pre_commit/runner.py deleted file mode 100644 index 53107007d..000000000 --- a/pre_commit/runner.py +++ /dev/null @@ -1,36 +0,0 @@ -from __future__ import unicode_literals - -import os.path - -from cached_property import cached_property - -from pre_commit import git -from pre_commit.clientlib import load_config - - -class Runner(object): - """A `Runner` represents the execution context of the hooks. Notably the - repository under test. - """ - - def __init__(self, git_root, config_file): - self.git_root = git_root - self.config_file = config_file - - @classmethod - def create(cls, config_file): - """Creates a Runner by doing the following: - - Finds the root of the current git repository - - chdir to that directory - """ - root = git.get_root() - os.chdir(root) - return cls(root, config_file) - - @property - def config_file_path(self): - return os.path.join(self.git_root, self.config_file) - - @cached_property - def config(self): - return load_config(self.config_file_path) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index b6e81b2a0..34c7292b0 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -13,12 +13,10 @@ from pre_commit.commands.autoupdate import _update_repo from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.autoupdate import RepositoryCannotBeUpdatedError -from pre_commit.runner import Runner from pre_commit.util import cmd_output from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import add_config_to_repo from testing.fixtures import config_with_local_hooks -from testing.fixtures import git_dir from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo from testing.fixtures import write_config @@ -45,7 +43,7 @@ def test_autoupdate_up_to_date_repo(up_to_date_repo, in_tmpdir, store): with open(C.CONFIG_FILE) as f: before = f.read() assert '^$' not in before - ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) + ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) with open(C.CONFIG_FILE) as f: after = f.read() assert ret == 0 @@ -72,7 +70,7 @@ def test_autoupdate_old_revision_broken(tempdir_factory, in_tmpdir, store): write_config('.', config) with open(C.CONFIG_FILE) as f: before = f.read() - ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) + ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) with open(C.CONFIG_FILE) as f: after = f.read() assert ret == 0 @@ -112,7 +110,7 @@ def test_autoupdate_out_of_date_repo(out_of_date_repo, in_tmpdir, store): with open(C.CONFIG_FILE) as f: before = f.read() - ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) + ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) with open(C.CONFIG_FILE) as f: after = f.read() assert ret == 0 @@ -133,11 +131,10 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( # Write out the config write_config('.', config) - runner = Runner('.', C.CONFIG_FILE) with open(C.CONFIG_FILE) as f: before = f.read() repo_name = 'file://{}'.format(out_of_date_repo.path) - ret = autoupdate(runner, store, tags_only=False, repos=(repo_name,)) + ret = autoupdate(C.CONFIG_FILE, store, tags_only=False, repos=(repo_name,)) with open(C.CONFIG_FILE) as f: after = f.read() assert ret == 0 @@ -155,11 +152,10 @@ def test_autoupdate_out_of_date_repo_with_wrong_repo_name( ) write_config('.', config) - runner = Runner('.', C.CONFIG_FILE) with open(C.CONFIG_FILE) as f: before = f.read() # It will not update it, because the name doesn't match - ret = autoupdate(runner, store, tags_only=False, repos=('dne',)) + ret = autoupdate(C.CONFIG_FILE, store, tags_only=False, repos=('dne',)) with open(C.CONFIG_FILE) as f: after = f.read() assert ret == 0 @@ -180,7 +176,7 @@ def test_does_not_reformat(in_tmpdir, out_of_date_repo, store): with open(C.CONFIG_FILE, 'w') as f: f.write(config) - autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) + autoupdate(C.CONFIG_FILE, store, tags_only=False) with open(C.CONFIG_FILE) as f: after = f.read() expected = fmt.format(out_of_date_repo.path, out_of_date_repo.head_rev) @@ -210,7 +206,7 @@ def test_loses_formatting_when_not_detectable( with open(C.CONFIG_FILE, 'w') as f: f.write(config) - autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) + autoupdate(C.CONFIG_FILE, store, tags_only=False) with open(C.CONFIG_FILE) as f: after = f.read() expected = ( @@ -235,7 +231,7 @@ def test_autoupdate_tagged_repo(tagged_repo, in_tmpdir, store): ) write_config('.', config) - ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) + ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) assert ret == 0 with open(C.CONFIG_FILE) as f: assert 'v1.2.3' in f.read() @@ -254,7 +250,7 @@ def test_autoupdate_tags_only(tagged_repo_with_more_commits, in_tmpdir, store): ) write_config('.', config) - ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=True) + ret = autoupdate(C.CONFIG_FILE, store, tags_only=True) assert ret == 0 with open(C.CONFIG_FILE) as f: assert 'v1.2.3' in f.read() @@ -269,7 +265,7 @@ def test_autoupdate_latest_no_config(out_of_date_repo, in_tmpdir, store): cmd_output('git', '-C', out_of_date_repo.path, 'rm', '-r', ':/') cmd_output('git', '-C', out_of_date_repo.path, 'commit', '-m', 'rm') - ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) + ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) assert ret == 1 with open(C.CONFIG_FILE) as f: assert out_of_date_repo.original_rev in f.read() @@ -313,20 +309,18 @@ def test_autoupdate_hook_disappearing_repo( with open(C.CONFIG_FILE) as f: before = f.read() - ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) + ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) with open(C.CONFIG_FILE) as f: after = f.read() assert ret == 1 assert before == after -def test_autoupdate_local_hooks(tempdir_factory, store): - git_path = git_dir(tempdir_factory) +def test_autoupdate_local_hooks(in_git_dir, store): config = config_with_local_hooks() - path = add_config_to_repo(git_path, config) - runner = Runner(path, C.CONFIG_FILE) - assert autoupdate(runner, store, tags_only=False) == 0 - new_config_writen = load_config(runner.config_file_path) + add_config_to_repo('.', config) + assert autoupdate(C.CONFIG_FILE, store, tags_only=False) == 0 + new_config_writen = load_config(C.CONFIG_FILE) assert len(new_config_writen['repos']) == 1 assert new_config_writen['repos'][0] == config @@ -340,9 +334,8 @@ def test_autoupdate_local_hooks_with_out_of_date_repo( local_config = config_with_local_hooks() config = {'repos': [local_config, stale_config]} write_config('.', config) - runner = Runner('.', C.CONFIG_FILE) - assert autoupdate(runner, store, tags_only=False) == 0 - new_config_writen = load_config(runner.config_file_path) + assert autoupdate(C.CONFIG_FILE, store, tags_only=False) == 0 + new_config_writen = load_config(C.CONFIG_FILE) assert len(new_config_writen['repos']) == 2 assert new_config_writen['repos'][0] == local_config @@ -355,8 +348,8 @@ def test_autoupdate_meta_hooks(tmpdir, capsys, store): ' hooks:\n' ' - id: check-useless-excludes\n', ) - runner = Runner(tmpdir.strpath, C.CONFIG_FILE) - ret = autoupdate(runner, store, tags_only=True) + with tmpdir.as_cwd(): + ret = autoupdate(C.CONFIG_FILE, store, tags_only=True) assert ret == 0 assert cfg.read() == ( 'repos:\n' @@ -376,8 +369,8 @@ def test_updates_old_format_to_new_format(tmpdir, capsys, store): ' entry: ./bin/foo.sh\n' ' language: script\n', ) - runner = Runner(tmpdir.strpath, C.CONFIG_FILE) - ret = autoupdate(runner, store, tags_only=True) + with tmpdir.as_cwd(): + ret = autoupdate(C.CONFIG_FILE, store, tags_only=True) assert ret == 0 contents = cfg.read() assert contents == ( diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index dbf663e97..25a216418 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -5,7 +5,6 @@ import io import os.path import re -import shutil import subprocess import sys @@ -20,7 +19,6 @@ from pre_commit.commands.install_uninstall import shebang from pre_commit.commands.install_uninstall import uninstall from pre_commit.languages import python -from pre_commit.runner import Runner from pre_commit.util import cmd_output from pre_commit.util import make_executable from pre_commit.util import mkdirp @@ -65,62 +63,45 @@ def test_shebang_returns_default(): assert shebang() == '#!/usr/bin/env python' -def test_install_pre_commit(tempdir_factory, store): - path = git_dir(tempdir_factory) - runner = Runner(path, C.CONFIG_FILE) - assert not install(runner, store) - assert os.access(os.path.join(path, '.git/hooks/pre-commit'), os.X_OK) +def test_install_pre_commit(in_git_dir, store): + assert not install(C.CONFIG_FILE, store) + assert os.access(in_git_dir.join('.git/hooks/pre-commit').strpath, os.X_OK) - assert not install(runner, store, hook_type='pre-push') - assert os.access(os.path.join(path, '.git/hooks/pre-push'), os.X_OK) + assert not install(C.CONFIG_FILE, store, hook_type='pre-push') + assert os.access(in_git_dir.join('.git/hooks/pre-push').strpath, os.X_OK) -def test_install_hooks_directory_not_present(tempdir_factory, store): - path = git_dir(tempdir_factory) +def test_install_hooks_directory_not_present(in_git_dir, store): # Simulate some git clients which don't make .git/hooks #234 - hooks = os.path.join(path, '.git/hooks') - if os.path.exists(hooks): # pragma: no cover (latest git) - shutil.rmtree(hooks) - runner = Runner(path, C.CONFIG_FILE) - install(runner, store) - assert os.path.exists(os.path.join(path, '.git/hooks/pre-commit')) + if in_git_dir.join('.git/hooks').exists(): # pragma: no cover (odd git) + in_git_dir.join('.git/hooks').remove() + install(C.CONFIG_FILE, store) + assert in_git_dir.join('.git/hooks/pre-commit').exists() -def test_install_refuses_core_hookspath(tempdir_factory, store): - path = git_dir(tempdir_factory) - with cwd(path): - cmd_output('git', 'config', '--local', 'core.hooksPath', 'hooks') - runner = Runner(path, C.CONFIG_FILE) - assert install(runner, store) +def test_install_refuses_core_hookspath(in_git_dir, store): + cmd_output('git', 'config', '--local', 'core.hooksPath', 'hooks') + assert install(C.CONFIG_FILE, store) -@xfailif_no_symlink -def test_install_hooks_dead_symlink( - tempdir_factory, store, -): # pragma: no cover (non-windows) - path = git_dir(tempdir_factory) - runner = Runner(path, C.CONFIG_FILE) - mkdirp(os.path.join(path, '.git/hooks')) - os.symlink('/fake/baz', os.path.join(path, '.git/hooks/pre-commit')) - install(runner, store) - assert os.path.exists(os.path.join(path, '.git/hooks/pre-commit')) +@xfailif_no_symlink # pragma: no cover (non-windows) +def test_install_hooks_dead_symlink(in_git_dir, store): + hook = in_git_dir.join('.git/hooks').ensure_dir().join('pre-commit') + os.symlink('/fake/baz', hook.strpath) + install(C.CONFIG_FILE, store) + assert hook.exists() -def test_uninstall_does_not_blow_up_when_not_there(tempdir_factory): - path = git_dir(tempdir_factory) - runner = Runner(path, C.CONFIG_FILE) - ret = uninstall(runner) - assert ret == 0 +def test_uninstall_does_not_blow_up_when_not_there(in_git_dir): + assert uninstall() == 0 -def test_uninstall(tempdir_factory, store): - path = git_dir(tempdir_factory) - runner = Runner(path, C.CONFIG_FILE) - assert not os.path.exists(os.path.join(path, '.git/hooks/pre-commit')) - install(runner, store) - assert os.path.exists(os.path.join(path, '.git/hooks/pre-commit')) - uninstall(runner) - assert not os.path.exists(os.path.join(path, '.git/hooks/pre-commit')) +def test_uninstall(in_git_dir, store): + assert not in_git_dir.join('.git/hooks/pre-commit').exists() + install(C.CONFIG_FILE, store) + assert in_git_dir.join('.git/hooks/pre-commit').exists() + uninstall() + assert not in_git_dir.join('.git/hooks/pre-commit').exists() def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): @@ -159,7 +140,7 @@ def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): def test_install_pre_commit_and_run(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - assert install(Runner(path, C.CONFIG_FILE), store) == 0 + assert install(C.CONFIG_FILE, store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -171,7 +152,7 @@ def test_install_pre_commit_and_run_custom_path(tempdir_factory, store): with cwd(path): cmd_output('git', 'mv', C.CONFIG_FILE, 'custom-config.yaml') cmd_output('git', 'commit', '-m', 'move pre-commit config') - assert install(Runner(path, 'custom-config.yaml'), store) == 0 + assert install('custom-config.yaml', store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -186,7 +167,7 @@ def test_install_in_submodule_and_run(tempdir_factory, store): sub_pth = os.path.join(parent_path, 'sub') with cwd(sub_pth): - assert install(Runner(sub_pth, C.CONFIG_FILE), store) == 0 + assert install(C.CONFIG_FILE, store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) @@ -199,7 +180,7 @@ def test_install_in_worktree_and_run(tempdir_factory, store): cmd_output('git', '-C', src_path, 'worktree', 'add', path, '-b', 'master') with cwd(path): - assert install(Runner(path, C.CONFIG_FILE), store) == 0 + assert install(C.CONFIG_FILE, store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) @@ -216,7 +197,7 @@ def test_commit_am(tempdir_factory, store): with io.open('unstaged', 'w') as foo_file: foo_file.write('Oh hai') - assert install(Runner(path, C.CONFIG_FILE), store) == 0 + assert install(C.CONFIG_FILE, store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -225,7 +206,7 @@ def test_commit_am(tempdir_factory, store): def test_unicode_merge_commit_message(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - assert install(Runner(path, C.CONFIG_FILE), store) == 0 + assert install(C.CONFIG_FILE, store) == 0 cmd_output('git', 'checkout', 'master', '-b', 'foo') cmd_output('git', 'commit', '--allow-empty', '-n', '-m', 'branch2') cmd_output('git', 'checkout', 'master') @@ -240,8 +221,8 @@ def test_unicode_merge_commit_message(tempdir_factory, store): def test_install_idempotent(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - assert install(Runner(path, C.CONFIG_FILE), store) == 0 - assert install(Runner(path, C.CONFIG_FILE), store) == 0 + assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -261,7 +242,7 @@ def test_environment_not_sourced(tempdir_factory, store): with cwd(path): # Patch the executable to simulate rming virtualenv with mock.patch.object(sys, 'executable', '/does-not-exist'): - assert install(Runner(path, C.CONFIG_FILE), store) == 0 + assert install(C.CONFIG_FILE, store) == 0 # Use a specific homedir to ignore --user installs homedir = tempdir_factory.get() @@ -300,7 +281,7 @@ def test_environment_not_sourced(tempdir_factory, store): def test_failing_hooks_returns_nonzero(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'failing_hook_repo') with cwd(path): - assert install(Runner(path, C.CONFIG_FILE), store) == 0 + assert install(C.CONFIG_FILE, store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 1 @@ -325,8 +306,6 @@ def _write_legacy_hook(path): def test_install_existing_hooks_no_overwrite(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path, C.CONFIG_FILE) - _write_legacy_hook(path) # Make sure we installed the "old" hook correctly @@ -335,7 +314,7 @@ def test_install_existing_hooks_no_overwrite(tempdir_factory, store): assert EXISTING_COMMIT_RUN.match(output) # Now install pre-commit (no-overwrite) - assert install(runner, store) == 0 + assert install(C.CONFIG_FILE, store) == 0 # We should run both the legacy and pre-commit hooks ret, output = _get_commit_output(tempdir_factory) @@ -347,13 +326,11 @@ def test_install_existing_hooks_no_overwrite(tempdir_factory, store): def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path, C.CONFIG_FILE) - _write_legacy_hook(path) # Install twice - assert install(runner, store) == 0 - assert install(runner, store) == 0 + assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store) == 0 # We should run both the legacy and pre-commit hooks ret, output = _get_commit_output(tempdir_factory) @@ -372,15 +349,13 @@ def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory, store): def test_failing_existing_hook_returns_1(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path, C.CONFIG_FILE) - # Write out a failing "old" hook mkdirp(os.path.join(path, '.git/hooks')) with io.open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: f.write('#!/usr/bin/env bash\necho "fail!"\nexit 1\n') make_executable(f.name) - assert install(runner, store) == 0 + assert install(C.CONFIG_FILE, store) == 0 # We should get a failure from the legacy hook ret, output = _get_commit_output(tempdir_factory) @@ -391,8 +366,7 @@ def test_failing_existing_hook_returns_1(tempdir_factory, store): def test_install_overwrite_no_existing_hooks(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path, C.CONFIG_FILE) - assert install(runner, store, overwrite=True) == 0 + assert install(C.CONFIG_FILE, store, overwrite=True) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -402,10 +376,8 @@ def test_install_overwrite_no_existing_hooks(tempdir_factory, store): def test_install_overwrite(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path, C.CONFIG_FILE) - _write_legacy_hook(path) - assert install(runner, store, overwrite=True) == 0 + assert install(C.CONFIG_FILE, store, overwrite=True) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -415,13 +387,11 @@ def test_install_overwrite(tempdir_factory, store): def test_uninstall_restores_legacy_hooks(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path, C.CONFIG_FILE) - _write_legacy_hook(path) # Now install and uninstall pre-commit - assert install(runner, store) == 0 - assert uninstall(runner) == 0 + assert install(C.CONFIG_FILE, store) == 0 + assert uninstall() == 0 # Make sure we installed the "old" hook correctly ret, output = _get_commit_output(tempdir_factory, touch_file='baz') @@ -432,8 +402,6 @@ def test_uninstall_restores_legacy_hooks(tempdir_factory, store): def test_replace_old_commit_script(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path, C.CONFIG_FILE) - # Install a script that looks like our old script pre_commit_contents = resource_text('hook-tmpl') new_contents = pre_commit_contents.replace( @@ -446,7 +414,7 @@ def test_replace_old_commit_script(tempdir_factory, store): make_executable(f.name) # Install normally - assert install(runner, store) == 0 + assert install(C.CONFIG_FILE, store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -456,13 +424,12 @@ def test_replace_old_commit_script(tempdir_factory, store): def test_uninstall_doesnt_remove_not_our_hooks(tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): - runner = Runner(path, C.CONFIG_FILE) mkdirp(os.path.join(path, '.git/hooks')) with io.open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: f.write('#!/usr/bin/env bash\necho 1\n') make_executable(f.name) - assert uninstall(runner) == 0 + assert uninstall() == 0 assert os.path.exists(os.path.join(path, '.git/hooks/pre-commit')) @@ -478,7 +445,7 @@ def test_uninstall_doesnt_remove_not_our_hooks(tempdir_factory): def test_installs_hooks_with_hooks_True(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - install(Runner(path, C.CONFIG_FILE), store, hooks=True) + install(C.CONFIG_FILE, store, hooks=True) ret, output = _get_commit_output( tempdir_factory, pre_commit_home=store.directory, ) @@ -490,9 +457,8 @@ def test_installs_hooks_with_hooks_True(tempdir_factory, store): def test_install_hooks_command(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path, C.CONFIG_FILE) - install(runner, store) - install_hooks(runner, store) + install(C.CONFIG_FILE, store) + install_hooks(C.CONFIG_FILE, store) ret, output = _get_commit_output( tempdir_factory, pre_commit_home=store.directory, ) @@ -504,7 +470,7 @@ def test_install_hooks_command(tempdir_factory, store): def test_installed_from_venv(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - install(Runner(path, C.CONFIG_FILE), store) + install(C.CONFIG_FILE, store) # No environment so pre-commit is not on the path when running! # Should still pick up the python from when we installed ret, output = _get_commit_output( @@ -543,7 +509,7 @@ def test_pre_push_integration_failing(tempdir_factory, store): path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - install(Runner(path, C.CONFIG_FILE), store, hook_type='pre-push') + install(C.CONFIG_FILE, store, hook_type='pre-push') # commit succeeds because pre-commit is only installed for pre-push assert _get_commit_output(tempdir_factory)[0] == 0 assert _get_commit_output(tempdir_factory, touch_file='zzz')[0] == 0 @@ -561,7 +527,7 @@ def test_pre_push_integration_accepted(tempdir_factory, store): path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - install(Runner(path, C.CONFIG_FILE), store, hook_type='pre-push') + install(C.CONFIG_FILE, store, hook_type='pre-push') assert _get_commit_output(tempdir_factory)[0] == 0 retc, output = _get_push_output(tempdir_factory) @@ -581,7 +547,7 @@ def test_pre_push_force_push_without_fetch(tempdir_factory, store): assert _get_push_output(tempdir_factory)[0] == 0 with cwd(path2): - install(Runner(path2, C.CONFIG_FILE), store, hook_type='pre-push') + install(C.CONFIG_FILE, store, hook_type='pre-push') assert _get_commit_output(tempdir_factory, commit_msg='force!')[0] == 0 retc, output = _get_push_output(tempdir_factory, opts=('--force',)) @@ -596,7 +562,7 @@ def test_pre_push_new_upstream(tempdir_factory, store): path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - install(Runner(path, C.CONFIG_FILE), store, hook_type='pre-push') + install(C.CONFIG_FILE, store, hook_type='pre-push') assert _get_commit_output(tempdir_factory)[0] == 0 cmd_output('git', 'remote', 'rename', 'origin', 'upstream') @@ -612,7 +578,7 @@ def test_pre_push_integration_empty_push(tempdir_factory, store): path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - install(Runner(path, C.CONFIG_FILE), store, hook_type='pre-push') + install(C.CONFIG_FILE, store, hook_type='pre-push') _get_push_output(tempdir_factory) retc, output = _get_push_output(tempdir_factory) assert output == 'Everything up-to-date\n' @@ -624,8 +590,6 @@ def test_pre_push_legacy(tempdir_factory, store): path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - runner = Runner(path, C.CONFIG_FILE) - mkdirp(os.path.join(path, '.git/hooks')) with io.open(os.path.join(path, '.git/hooks/pre-push'), 'w') as f: f.write( @@ -637,7 +601,7 @@ def test_pre_push_legacy(tempdir_factory, store): ) make_executable(f.name) - install(runner, store, hook_type='pre-push') + install(C.CONFIG_FILE, store, hook_type='pre-push') assert _get_commit_output(tempdir_factory)[0] == 0 retc, output = _get_push_output(tempdir_factory) @@ -651,8 +615,7 @@ def test_pre_push_legacy(tempdir_factory, store): def test_commit_msg_integration_failing( commit_msg_repo, tempdir_factory, store, ): - runner = Runner(commit_msg_repo, C.CONFIG_FILE) - install(runner, store, hook_type='commit-msg') + install(C.CONFIG_FILE, store, hook_type='commit-msg') retc, out = _get_commit_output(tempdir_factory) assert retc == 1 assert out.startswith('Must have "Signed off by:"...') @@ -662,8 +625,7 @@ def test_commit_msg_integration_failing( def test_commit_msg_integration_passing( commit_msg_repo, tempdir_factory, store, ): - runner = Runner(commit_msg_repo, C.CONFIG_FILE) - install(runner, store, hook_type='commit-msg') + install(C.CONFIG_FILE, store, hook_type='commit-msg') msg = 'Hi\nSigned off by: me, lol' retc, out = _get_commit_output(tempdir_factory, commit_msg=msg) assert retc == 0 @@ -673,8 +635,6 @@ def test_commit_msg_integration_passing( def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): - runner = Runner(commit_msg_repo, C.CONFIG_FILE) - hook_path = os.path.join(commit_msg_repo, '.git/hooks/commit-msg') mkdirp(os.path.dirname(hook_path)) with io.open(hook_path, 'w') as hook_file: @@ -686,7 +646,7 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): ) make_executable(hook_path) - install(runner, store, hook_type='commit-msg') + install(C.CONFIG_FILE, store, hook_type='commit-msg') msg = 'Hi\nSigned off by: asottile' retc, out = _get_commit_output(tempdir_factory, commit_msg=msg) @@ -699,11 +659,9 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): def test_install_disallow_mising_config(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path, C.CONFIG_FILE) - remove_config_from_repo(path) ret = install( - runner, store, overwrite=True, skip_on_missing_conf=False, + C.CONFIG_FILE, store, overwrite=True, skip_on_missing_conf=False, ) assert ret == 0 @@ -714,11 +672,9 @@ def test_install_disallow_mising_config(tempdir_factory, store): def test_install_allow_mising_config(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path, C.CONFIG_FILE) - remove_config_from_repo(path) ret = install( - runner, store, overwrite=True, skip_on_missing_conf=True, + C.CONFIG_FILE, store, overwrite=True, skip_on_missing_conf=True, ) assert ret == 0 @@ -734,11 +690,9 @@ def test_install_allow_mising_config(tempdir_factory, store): def test_install_temporarily_allow_mising_config(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path, C.CONFIG_FILE) - remove_config_from_repo(path) ret = install( - runner, store, overwrite=True, skip_on_missing_conf=False, + C.CONFIG_FILE, store, overwrite=True, skip_on_missing_conf=False, ) assert ret == 0 diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index a2a34b665..da599f10a 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -6,7 +6,6 @@ import pre_commit.constants as C from pre_commit.commands.migrate_config import _indent from pre_commit.commands.migrate_config import migrate_config -from pre_commit.runner import Runner @pytest.mark.parametrize( @@ -33,7 +32,8 @@ def test_migrate_config_normal_format(tmpdir, capsys): ' entry: ./bin/foo.sh\n' ' language: script\n', ) - assert not migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) + with tmpdir.as_cwd(): + assert not migrate_config(C.CONFIG_FILE) out, _ = capsys.readouterr() assert out == 'Configuration has been migrated.\n' contents = cfg.read() @@ -61,7 +61,8 @@ def test_migrate_config_document_marker(tmpdir): ' entry: ./bin/foo.sh\n' ' language: script\n', ) - assert not migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) + with tmpdir.as_cwd(): + assert not migrate_config(C.CONFIG_FILE) contents = cfg.read() assert contents == ( '# comment\n' @@ -88,7 +89,8 @@ def test_migrate_config_list_literal(tmpdir): ' }]\n' '}]', ) - assert not migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) + with tmpdir.as_cwd(): + assert not migrate_config(C.CONFIG_FILE) contents = cfg.read() assert contents == ( 'repos:\n' @@ -114,7 +116,8 @@ def test_already_migrated_configuration_noop(tmpdir, capsys): ) cfg = tmpdir.join(C.CONFIG_FILE) cfg.write(contents) - assert not migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) + with tmpdir.as_cwd(): + assert not migrate_config(C.CONFIG_FILE) out, _ = capsys.readouterr() assert out == 'Configuration is already migrated.\n' assert cfg.read() == contents @@ -133,7 +136,8 @@ def test_migrate_config_sha_to_rev(tmpdir): ) cfg = tmpdir.join(C.CONFIG_FILE) cfg.write(contents) - assert not migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) + with tmpdir.as_cwd(): + assert not migrate_config(C.CONFIG_FILE) contents = cfg.read() assert contents == ( 'repos:\n' diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index e6258d31b..bb233f285 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -16,7 +16,6 @@ from pre_commit.commands.run import _get_skips from pre_commit.commands.run import _has_unmerged_paths from pre_commit.commands.run import run -from pre_commit.runner import Runner from pre_commit.util import cmd_output from pre_commit.util import make_executable from testing.fixtures import add_config_to_repo @@ -49,9 +48,8 @@ def stage_a_file(filename='foo.py'): def _do_run(cap_out, store, repo, args, environ={}, config_file=C.CONFIG_FILE): - runner = Runner(repo, config_file) - with cwd(runner.git_root): # replicates Runner.create behaviour - ret = run(runner, store, args, environ=environ) + with cwd(repo): # replicates `main._adjust_args_and_chdir` behaviour + ret = run(config_file, store, args, environ=environ) printed = cap_out.get_bytes() return ret, printed @@ -435,7 +433,7 @@ def test_stdout_write_bug_py26(repo_with_failing_hook, store, tempdir_factory): config['repos'][0]['hooks'][0]['args'] = ['☃'] stage_a_file() - install(Runner(repo_with_failing_hook, C.CONFIG_FILE), store) + install(C.CONFIG_FILE, store) # Have to use subprocess because pytest monkeypatches sys.stdout _, stdout, _ = cmd_output_mocked_pre_commit_home( @@ -465,7 +463,7 @@ def test_lots_of_files(store, tempdir_factory): open(filename, 'w').close() cmd_output('git', 'add', '.') - install(Runner(git_path, C.CONFIG_FILE), store) + install(C.CONFIG_FILE, store) cmd_output_mocked_pre_commit_home( 'git', 'commit', '-m', 'Commit!', diff --git a/tests/conftest.py b/tests/conftest.py index 82daccd4d..95fc410e8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -63,6 +63,13 @@ def in_tmpdir(tempdir_factory): yield path +@pytest.fixture +def in_git_dir(tmpdir): + with tmpdir.as_cwd(): + cmd_output('git', 'init') + yield tmpdir + + def _make_conflict(): cmd_output('git', 'checkout', 'origin/master', '-b', 'foo') with io.open('conflict_file', 'w') as conflict_file: diff --git a/tests/main_test.py b/tests/main_test.py index 65adc477a..83e7d22f4 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -7,9 +7,44 @@ import mock import pytest +import pre_commit.constants as C from pre_commit import main from testing.auto_namedtuple import auto_namedtuple -from testing.util import cwd + + +class Args(object): + def __init__(self, **kwargs): + kwargs.setdefault('command', 'help') + kwargs.setdefault('config', C.CONFIG_FILE) + self.__dict__.update(kwargs) + + +def test_adjust_args_and_chdir_noop(in_git_dir): + args = Args(command='run', files=['f1', 'f2']) + main._adjust_args_and_chdir(args) + assert os.getcwd() == in_git_dir + assert args.config == C.CONFIG_FILE + assert args.files == ['f1', 'f2'] + + +def test_adjust_args_and_chdir_relative_things(in_git_dir): + in_git_dir.join('foo/cfg.yaml').ensure() + in_git_dir.join('foo').chdir() + + args = Args(command='run', files=['f1', 'f2'], config='cfg.yaml') + main._adjust_args_and_chdir(args) + assert os.getcwd() == in_git_dir + assert args.config == os.path.join('foo', 'cfg.yaml') + assert args.files == [os.path.join('foo', 'f1'), os.path.join('foo', 'f2')] + + +def test_adjust_args_and_chdir_non_relative_config(in_git_dir): + in_git_dir.join('foo').ensure_dir().chdir() + + args = Args() + main._adjust_args_and_chdir(args) + assert os.getcwd() == in_git_dir + assert args.config == C.CONFIG_FILE FNS = ( @@ -28,18 +63,6 @@ def mock_commands(): mck.stop() -class CalledExit(Exception): - pass - - -@pytest.fixture -def argparse_exit_mock(): - with mock.patch.object( - argparse.ArgumentParser, 'exit', side_effect=CalledExit, - ) as exit_mock: - yield exit_mock - - @pytest.fixture def argparse_parse_args_spy(): parse_args_mock = mock.Mock() @@ -62,15 +85,13 @@ def assert_only_one_mock_called(mock_objs): assert total_call_count == 1 -def test_overall_help(mock_commands, argparse_exit_mock): - with pytest.raises(CalledExit): +def test_overall_help(mock_commands): + with pytest.raises(SystemExit): main.main(['--help']) -def test_help_command( - mock_commands, argparse_exit_mock, argparse_parse_args_spy, -): - with pytest.raises(CalledExit): +def test_help_command(mock_commands, argparse_parse_args_spy): + with pytest.raises(SystemExit): main.main(['help']) argparse_parse_args_spy.assert_has_calls([ @@ -79,10 +100,8 @@ def test_help_command( ]) -def test_help_other_command( - mock_commands, argparse_exit_mock, argparse_parse_args_spy, -): - with pytest.raises(CalledExit): +def test_help_other_command(mock_commands, argparse_parse_args_spy): + with pytest.raises(SystemExit): main.main(['help', 'run']) argparse_parse_args_spy.assert_has_calls([ @@ -105,16 +124,12 @@ def test_try_repo(mock_store_dir): def test_help_cmd_in_empty_directory( + in_tmpdir, mock_commands, - tempdir_factory, - argparse_exit_mock, argparse_parse_args_spy, ): - path = tempdir_factory.get() - - with cwd(path): - with pytest.raises(CalledExit): - main.main(['help', 'run']) + with pytest.raises(SystemExit): + main.main(['help', 'run']) argparse_parse_args_spy.assert_has_calls([ mock.call(['help', 'run']), @@ -122,12 +137,9 @@ def test_help_cmd_in_empty_directory( ]) -def test_expected_fatal_error_no_git_repo( - tempdir_factory, cap_out, mock_store_dir, -): - with cwd(tempdir_factory.get()): - with pytest.raises(SystemExit): - main.main([]) +def test_expected_fatal_error_no_git_repo(in_tmpdir, cap_out, mock_store_dir): + with pytest.raises(SystemExit): + main.main([]) log_file = os.path.join(mock_store_dir, 'pre-commit.log') assert cap_out.get() == ( 'An error has occurred: FatalError: git failed. ' diff --git a/tests/runner_test.py b/tests/runner_test.py deleted file mode 100644 index 8d1c0421d..000000000 --- a/tests/runner_test.py +++ /dev/null @@ -1,44 +0,0 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - -import os.path - -import pre_commit.constants as C -from pre_commit.runner import Runner -from testing.fixtures import git_dir -from testing.util import cwd - - -def test_init_has_no_side_effects(tmpdir): - current_wd = os.getcwd() - runner = Runner(tmpdir.strpath, C.CONFIG_FILE) - assert runner.git_root == tmpdir.strpath - assert os.getcwd() == current_wd - - -def test_create_sets_correct_directory(tempdir_factory): - path = git_dir(tempdir_factory) - with cwd(path): - runner = Runner.create(C.CONFIG_FILE) - assert os.path.normcase(runner.git_root) == os.path.normcase(path) - assert os.path.normcase(os.getcwd()) == os.path.normcase(path) - - -def test_create_changes_to_git_root(tempdir_factory): - path = git_dir(tempdir_factory) - with cwd(path): - # Change into some directory, create should set to root - foo_path = os.path.join(path, 'foo') - os.mkdir(foo_path) - os.chdir(foo_path) - assert os.getcwd() != path - - runner = Runner.create(C.CONFIG_FILE) - assert os.path.normcase(runner.git_root) == os.path.normcase(path) - assert os.path.normcase(os.getcwd()) == os.path.normcase(path) - - -def test_config_file_path(): - runner = Runner(os.path.join('foo', 'bar'), C.CONFIG_FILE) - expected_path = os.path.join('foo', 'bar', C.CONFIG_FILE) - assert runner.config_file_path == expected_path diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 42f7ecae5..73a6b585c 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -297,13 +297,6 @@ def test_non_utf8_conflicting_diff(foo_staged, patch_dir): _test_foo_state(foo_staged, contents, 'AM', encoding='latin-1') -@pytest.fixture -def in_git_dir(tmpdir): - with tmpdir.as_cwd(): - cmd_output('git', 'init', '.') - yield tmpdir - - def _write(b): with open('foo', 'wb') as f: f.write(b) From 2b8291d18fc50fc21e7ad5a9979e1b8cb9712f53 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 26 Dec 2018 22:45:13 -0800 Subject: [PATCH 302/544] add a no-cover for py3 [ci skip] --- pre_commit/languages/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 7ab117bf1..28b9cb879 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -75,7 +75,7 @@ def _shuffled(seq): fixed_random = random.Random() if six.PY2: # pragma: no cover (py2) fixed_random.seed(FIXED_RANDOM_SEED) - else: + else: # pragma: no cover (py3) fixed_random.seed(FIXED_RANDOM_SEED, version=1) seq = list(seq) From b096c0b8f2074ec5c7e05528b27be4a1bf3df8d7 Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Fri, 14 Dec 2018 12:29:52 +0000 Subject: [PATCH 303/544] Allow aliasing a hook and calling it by it's alias --- pre_commit/clientlib.py | 18 +++++++++++++++++- pre_commit/commands/run.py | 8 ++++++-- tests/commands/run_test.py | 26 ++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 2fa7b1535..0722f5e6c 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -29,6 +29,20 @@ def _make_argparser(filenames_help): return parser +class OptionalAlias(object): + + def check(self, dct): + if 'alias' in dct: + cfgv.check_string(dct['alias']) + + def apply_default(self, dct): + if 'alias' not in dct: + dct['alias'] = dct['id'] + + def remove_default(self, dct): + pass + + MANIFEST_HOOK_DICT = cfgv.Map( 'Hook', 'id', @@ -36,6 +50,7 @@ def _make_argparser(filenames_help): cfgv.Required('name', cfgv.check_string), cfgv.Required('entry', cfgv.check_string), cfgv.Required('language', cfgv.check_one_of(all_languages)), + cfgv.OptionalNoDefault('alias', cfgv.check_string), cfgv.Optional( 'files', cfgv.check_and(cfgv.check_string, cfgv.check_regex), '', @@ -125,6 +140,7 @@ def remove_default(self, dct): 'Hook', 'id', cfgv.Required('id', cfgv.check_string), + OptionalAlias(), # All keys in manifest hook dict are valid in a config hook dict, but # are optional. @@ -133,7 +149,7 @@ def remove_default(self, dct): *[ cfgv.OptionalNoDefault(item.key, item.check_fn) for item in MANIFEST_HOOK_DICT.items - if item.key != 'id' + if item.key not in ('id', 'alias') ] ) CONFIG_REPO_DICT = cfgv.Map( diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index f2ff7b38c..9cd3dfcf5 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -257,13 +257,17 @@ def run(config_file, store, args, environ=os.environ): for repo in repositories(config, store): for _, hook in repo.hooks: if ( - (not args.hook or hook['id'] == args.hook) and + (not args.hook or hook['id'] == args.hook or ( + hook['alias'] and hook['alias'] == args.hook + )) and (not hook['stages'] or args.hook_stage in hook['stages']) ): repo_hooks.append((repo, hook)) if args.hook and not repo_hooks: - output.write_line('No hook with id `{}`'.format(args.hook)) + output.write_line( + 'No hook with id or alias `{}`'.format(args.hook), + ) return 1 for repo in {repo for repo, _ in repo_hooks}: diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index bb233f285..1cec51f29 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -416,6 +416,32 @@ def test_multiple_hooks_same_id(cap_out, store, repo_with_passing_hook): assert output.count(b'Bash hook') == 2 +def test_aliased_hook_run(cap_out, store, repo_with_passing_hook): + with cwd(repo_with_passing_hook): + # Add bash hook on there again, aliased + with modify_config() as config: + config['repos'][0]['hooks'].append( + {'id': 'bash_hook', 'alias': 'foo_bash'}, + ) + stage_a_file() + + ret, output = _do_run( + cap_out, store, repo_with_passing_hook, + run_opts(verbose=True, hook='bash_hook'), + ) + assert ret == 0 + # Both hooks will run since they share the same ID + assert output.count(b'Bash hook') == 2 + + ret, output = _do_run( + cap_out, store, repo_with_passing_hook, + run_opts(verbose=True, hook='foo_bash'), + ) + assert ret == 0 + # Only the aliased hook runs + assert output.count(b'Bash hook') == 1 + + def test_non_ascii_hook_id(repo_with_passing_hook, tempdir_factory): with cwd(repo_with_passing_hook): _, stdout, _ = cmd_output_mocked_pre_commit_home( From afbc57f2ad135c54677347142462075df379238b Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Mon, 17 Dec 2018 12:05:55 +0000 Subject: [PATCH 304/544] Go back to optional. Requires less changes to existing code. --- pre_commit/clientlib.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 0722f5e6c..44599ea6b 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -29,20 +29,6 @@ def _make_argparser(filenames_help): return parser -class OptionalAlias(object): - - def check(self, dct): - if 'alias' in dct: - cfgv.check_string(dct['alias']) - - def apply_default(self, dct): - if 'alias' not in dct: - dct['alias'] = dct['id'] - - def remove_default(self, dct): - pass - - MANIFEST_HOOK_DICT = cfgv.Map( 'Hook', 'id', @@ -50,7 +36,7 @@ def remove_default(self, dct): cfgv.Required('name', cfgv.check_string), cfgv.Required('entry', cfgv.check_string), cfgv.Required('language', cfgv.check_one_of(all_languages)), - cfgv.OptionalNoDefault('alias', cfgv.check_string), + cfgv.Optional('alias', cfgv.check_string, ''), cfgv.Optional( 'files', cfgv.check_and(cfgv.check_string, cfgv.check_regex), '', @@ -140,7 +126,6 @@ def remove_default(self, dct): 'Hook', 'id', cfgv.Required('id', cfgv.check_string), - OptionalAlias(), # All keys in manifest hook dict are valid in a config hook dict, but # are optional. @@ -149,7 +134,7 @@ def remove_default(self, dct): *[ cfgv.OptionalNoDefault(item.key, item.check_fn) for item in MANIFEST_HOOK_DICT.items - if item.key not in ('id', 'alias') + if item.key != 'id' ] ) CONFIG_REPO_DICT = cfgv.Map( From 5840f880a92135599d868098645eb2aa7e3930de Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Wed, 26 Dec 2018 08:56:30 +0000 Subject: [PATCH 305/544] Address review comments and test failures --- pre_commit/commands/run.py | 17 ++++++++++------- tests/repository_test.py | 1 + 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 9cd3dfcf5..713603b30 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -257,17 +257,20 @@ def run(config_file, store, args, environ=os.environ): for repo in repositories(config, store): for _, hook in repo.hooks: if ( - (not args.hook or hook['id'] == args.hook or ( - hook['alias'] and hook['alias'] == args.hook - )) and - (not hook['stages'] or args.hook_stage in hook['stages']) + ( + not args.hook or + hook['id'] == args.hook or + hook['alias'] == args.hook + ) and + ( + not hook['stages'] or + args.hook_stage in hook['stages'] + ) ): repo_hooks.append((repo, hook)) if args.hook and not repo_hooks: - output.write_line( - 'No hook with id or alias `{}`'.format(args.hook), - ) + output.write_line('No hook with id `{}`'.format(args.hook)) return 1 for repo in {repo for repo, _ in repo_hooks}: diff --git a/tests/repository_test.py b/tests/repository_test.py index f1b0f6e02..4d851f599 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -831,6 +831,7 @@ def test_manifest_hooks(tempdir_factory, store): 'exclude': '^$', 'files': '', 'id': 'bash_hook', + 'alias': '', 'language': 'script', 'language_version': 'default', 'log_file': '', From 79c8b1fceb4ddf6f396f7d46ac1168faee2ffb6e Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Wed, 26 Dec 2018 09:05:37 +0000 Subject: [PATCH 306/544] Allow hook alias to be used in `SKIP`. Includes test. --- pre_commit/commands/run.py | 9 +++++++++ tests/commands/run_test.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 713603b30..2fb107b70 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -86,6 +86,15 @@ def _run_single_hook(filenames, hook, repo, args, skips, cols): cols=cols, )) return 0 + elif hook['alias'] and hook['alias'] in skips: + output.write(get_hook_message( + _hook_msg_start(hook, args.verbose), + end_msg=SKIPPED, + end_color=color.YELLOW, + use_color=args.color, + cols=cols, + )) + return 0 elif not filenames and not hook['always_run']: output.write(get_hook_message( _hook_msg_start(hook, args.verbose), diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 1cec51f29..c3d1ec5d9 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -388,6 +388,38 @@ def test_skip_hook(cap_out, store, repo_with_passing_hook): assert msg in printed +def test_skip_aliased_hook(cap_out, store, repo_with_passing_hook): + with cwd(repo_with_passing_hook): + # Add bash hook on there again, aliased + with modify_config() as config: + config['repos'][0]['hooks'].append( + {'id': 'bash_hook', 'alias': 'foo_bash'}, + ) + stage_a_file() + + ret, printed = _do_run( + cap_out, store, repo_with_passing_hook, + run_opts(hook='bash_hook'), + {'SKIP': 'bash_hook'}, + ) + assert ret == 0 + # Both hooks will run since they share the same ID + assert printed.count(b'Bash hook') == 2 + for msg in (b'Bash hook', b'Skipped'): + assert msg in printed + + ret, printed = _do_run( + cap_out, store, repo_with_passing_hook, + run_opts(hook='foo_bash'), + {'SKIP': 'foo_bash'}, + ) + assert ret == 0 + # Only the aliased hook runs + assert printed.count(b'Bash hook') == 1 + for msg in (b'Bash hook', b'Skipped'): + assert msg in printed, printed + + def test_hook_id_not_in_non_verbose_output( cap_out, store, repo_with_passing_hook, ): From 8ffd1f69d7684a6303471a377df314d2308a05c9 Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Thu, 27 Dec 2018 12:03:09 +0000 Subject: [PATCH 307/544] Address review comments --- pre_commit/commands/run.py | 11 +---------- tests/commands/run_test.py | 40 +++++++++++++++++--------------------- 2 files changed, 19 insertions(+), 32 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 2fb107b70..d9280460b 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -77,16 +77,7 @@ def _run_single_hook(filenames, hook, repo, args, skips, cols): 'replacement.'.format(hook['id'], repo.repo_config['repo']), ) - if hook['id'] in skips: - output.write(get_hook_message( - _hook_msg_start(hook, args.verbose), - end_msg=SKIPPED, - end_color=color.YELLOW, - use_color=args.color, - cols=cols, - )) - return 0 - elif hook['alias'] and hook['alias'] in skips: + if hook['id'] in skips or hook['alias'] in skips: output.write(get_hook_message( _hook_msg_start(hook, args.verbose), end_msg=SKIPPED, diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index c3d1ec5d9..37e17a523 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -42,6 +42,18 @@ def repo_with_failing_hook(tempdir_factory): yield git_path +@pytest.fixture +def aliased_repo(tempdir_factory): + git_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + with cwd(git_path): + with modify_config() as config: + config['repos'][0]['hooks'].append( + {'id': 'bash_hook', 'alias': 'foo_bash'}, + ) + stage_a_file() + yield git_path + + def stage_a_file(filename='foo.py'): open(filename, 'a').close() cmd_output('git', 'add', filename) @@ -388,17 +400,9 @@ def test_skip_hook(cap_out, store, repo_with_passing_hook): assert msg in printed -def test_skip_aliased_hook(cap_out, store, repo_with_passing_hook): - with cwd(repo_with_passing_hook): - # Add bash hook on there again, aliased - with modify_config() as config: - config['repos'][0]['hooks'].append( - {'id': 'bash_hook', 'alias': 'foo_bash'}, - ) - stage_a_file() - +def test_skip_aliased_hook(cap_out, store, aliased_repo): ret, printed = _do_run( - cap_out, store, repo_with_passing_hook, + cap_out, store, aliased_repo, run_opts(hook='bash_hook'), {'SKIP': 'bash_hook'}, ) @@ -409,7 +413,7 @@ def test_skip_aliased_hook(cap_out, store, repo_with_passing_hook): assert msg in printed ret, printed = _do_run( - cap_out, store, repo_with_passing_hook, + cap_out, store, aliased_repo, run_opts(hook='foo_bash'), {'SKIP': 'foo_bash'}, ) @@ -448,17 +452,9 @@ def test_multiple_hooks_same_id(cap_out, store, repo_with_passing_hook): assert output.count(b'Bash hook') == 2 -def test_aliased_hook_run(cap_out, store, repo_with_passing_hook): - with cwd(repo_with_passing_hook): - # Add bash hook on there again, aliased - with modify_config() as config: - config['repos'][0]['hooks'].append( - {'id': 'bash_hook', 'alias': 'foo_bash'}, - ) - stage_a_file() - +def test_aliased_hook_run(cap_out, store, aliased_repo): ret, output = _do_run( - cap_out, store, repo_with_passing_hook, + cap_out, store, aliased_repo, run_opts(verbose=True, hook='bash_hook'), ) assert ret == 0 @@ -466,7 +462,7 @@ def test_aliased_hook_run(cap_out, store, repo_with_passing_hook): assert output.count(b'Bash hook') == 2 ret, output = _do_run( - cap_out, store, repo_with_passing_hook, + cap_out, store, aliased_repo, run_opts(verbose=True, hook='foo_bash'), ) assert ret == 0 From 6d40b2a38b274e7a561322749702a00703432a66 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 27 Dec 2018 09:24:41 -0800 Subject: [PATCH 308/544] Simplify the skip test to only test skipping --- tests/commands/run_test.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 37e17a523..bc891c0cf 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -401,27 +401,15 @@ def test_skip_hook(cap_out, store, repo_with_passing_hook): def test_skip_aliased_hook(cap_out, store, aliased_repo): - ret, printed = _do_run( - cap_out, store, aliased_repo, - run_opts(hook='bash_hook'), - {'SKIP': 'bash_hook'}, - ) - assert ret == 0 - # Both hooks will run since they share the same ID - assert printed.count(b'Bash hook') == 2 - for msg in (b'Bash hook', b'Skipped'): - assert msg in printed - ret, printed = _do_run( cap_out, store, aliased_repo, run_opts(hook='foo_bash'), {'SKIP': 'foo_bash'}, ) assert ret == 0 - # Only the aliased hook runs - assert printed.count(b'Bash hook') == 1 + # Only the aliased hook runs and is skipped for msg in (b'Bash hook', b'Skipped'): - assert msg in printed, printed + assert printed.count(msg) == 1 def test_hook_id_not_in_non_verbose_output( From 2af0b0b4f3ef670e67e896b690ed07dd13ade595 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 27 Dec 2018 17:31:25 -0800 Subject: [PATCH 309/544] better no-cover for windows --- pre_commit/file_lock.py | 4 ++-- pre_commit/languages/node.py | 2 +- tests/commands/install_uninstall_test.py | 2 +- tests/commands/run_test.py | 4 ++-- tests/languages/python_test.py | 2 +- tests/repository_test.py | 12 ++++-------- 6 files changed, 11 insertions(+), 15 deletions(-) diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index 7c7e85143..cf9aeac5a 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -41,14 +41,14 @@ def _locked(fileno, blocked_cb): # "Regions should be locked only briefly and should be unlocked # before closing a file or exiting the program." msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region) -except ImportError: # pragma: no cover (posix) +except ImportError: # pragma: windows no cover import fcntl @contextlib.contextmanager def _locked(fileno, blocked_cb): try: fcntl.flock(fileno, fcntl.LOCK_EX | fcntl.LOCK_NB) - except IOError: + except IOError: # pragma: no cover (tests are single-threaded) blocked_cb() fcntl.flock(fileno, fcntl.LOCK_EX) try: diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 8e5dc7e5c..2e9e60e4f 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -28,7 +28,7 @@ def get_env_patch(venv): install_prefix = r'{}\bin'.format(win_venv.strip()) elif sys.platform == 'win32': # pragma: no cover install_prefix = bin_dir(venv) - else: + else: # pragma: windows no cover install_prefix = venv return ( ('NODE_VIRTUAL_ENV', venv), diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 25a216418..401a1decf 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -84,7 +84,7 @@ def test_install_refuses_core_hookspath(in_git_dir, store): assert install(C.CONFIG_FILE, store) -@xfailif_no_symlink # pragma: no cover (non-windows) +@xfailif_no_symlink # pragma: windows no cover def test_install_hooks_dead_symlink(in_git_dir, store): hook = in_git_dir.join('.git/hooks').ensure_dir().join('pre-commit') os.symlink('/fake/baz', hook.strpath) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index bc891c0cf..33920e5e7 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -781,8 +781,8 @@ def test_include_exclude_base_case(some_filenames): ] -@xfailif_no_symlink -def test_matches_broken_symlink(tmpdir): # pragma: no cover (non-windows) +@xfailif_no_symlink # pragma: windows no cover +def test_matches_broken_symlink(tmpdir): with tmpdir.as_cwd(): os.symlink('does-not-exist', 'link') ret = _filter_by_include_exclude({'link'}, '', '^$') diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 78211cb9a..366c010e6 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -11,7 +11,7 @@ def test_norm_version_expanduser(): if os.name == 'nt': # pragma: no cover (nt) path = r'~\python343' expected_path = r'{}\python343'.format(home) - else: # pragma: no cover (non-nt) + else: # pragma: windows no cover path = '~/.pyenv/versions/3.4.3/bin/python' expected_path = home + '/.pyenv/versions/3.4.3/bin/python' result = python.norm_version(path) diff --git a/tests/repository_test.py b/tests/repository_test.py index 4d851f599..929640372 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -502,10 +502,8 @@ def test_additional_dependencies_roll_forward(tempdir_factory, store): assert 'mccabe' not in cmd_output('pip', 'freeze', '-l')[1] -@xfailif_windows_no_ruby -def test_additional_ruby_dependencies_installed( - tempdir_factory, store, -): # pragma: no cover (non-windows) +@xfailif_windows_no_ruby # pragma: windows no cover +def test_additional_ruby_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'ruby_hooks_repo') config = make_config_from_repo(path) config['hooks'][0]['additional_dependencies'] = ['thread_safe', 'tins'] @@ -518,10 +516,8 @@ def test_additional_ruby_dependencies_installed( assert 'tins' in output -@xfailif_broken_deep_listdir -def test_additional_node_dependencies_installed( - tempdir_factory, store, -): # pragma: no cover (non-windows) +@xfailif_broken_deep_listdir # pragma: windows no cover +def test_additional_node_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'node_hooks_repo') config = make_config_from_repo(path) # Careful to choose a small package that's not depped by npm From d46bbc486fa81cbbf80504d286efa5684ce05eb6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 27 Dec 2018 18:02:14 -0800 Subject: [PATCH 310/544] Use in_git_dir in more places --- tests/commands/install_uninstall_test.py | 15 ++-- tests/conftest.py | 5 +- tests/git_test.py | 69 ++++++++----------- tests/meta_hooks/check_hooks_apply_test.py | 44 ++++-------- .../meta_hooks/check_useless_excludes_test.py | 42 ++++------- tests/repository_test.py | 31 ++++----- tests/staged_files_only_test.py | 25 +++---- 7 files changed, 88 insertions(+), 143 deletions(-) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 401a1decf..ce74a2ea1 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -421,17 +421,14 @@ def test_replace_old_commit_script(tempdir_factory, store): assert NORMAL_PRE_COMMIT_RUN.match(output) -def test_uninstall_doesnt_remove_not_our_hooks(tempdir_factory): - path = git_dir(tempdir_factory) - with cwd(path): - mkdirp(os.path.join(path, '.git/hooks')) - with io.open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: - f.write('#!/usr/bin/env bash\necho 1\n') - make_executable(f.name) +def test_uninstall_doesnt_remove_not_our_hooks(in_git_dir): + pre_commit = in_git_dir.join('.git/hooks').ensure_dir().join('pre-commit') + pre_commit.write('#!/usr/bin/env bash\necho 1\n') + make_executable(pre_commit.strpath) - assert uninstall() == 0 + assert uninstall() == 0 - assert os.path.exists(os.path.join(path, '.git/hooks/pre-commit')) + assert pre_commit.exists() PRE_INSTALLED = re.compile( diff --git a/tests/conftest.py b/tests/conftest.py index 95fc410e8..49fbf3fc1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -65,9 +65,10 @@ def in_tmpdir(tempdir_factory): @pytest.fixture def in_git_dir(tmpdir): - with tmpdir.as_cwd(): + repo = tmpdir.join('repo').ensure_dir() + with repo.as_cwd(): cmd_output('git', 'init') - yield tmpdir + yield repo def _make_conflict(): diff --git a/tests/git_test.py b/tests/git_test.py index 58f14f50a..2a9bda4a2 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -9,45 +9,34 @@ from pre_commit import git from pre_commit.error_handler import FatalError from pre_commit.util import cmd_output -from testing.fixtures import git_dir -from testing.util import cwd -def test_get_root_at_root(tempdir_factory): - path = git_dir(tempdir_factory) - with cwd(path): - assert os.path.normcase(git.get_root()) == os.path.normcase(path) +def test_get_root_at_root(in_git_dir): + expected = os.path.normcase(in_git_dir.strpath) + assert os.path.normcase(git.get_root()) == expected -def test_get_root_deeper(tempdir_factory): - path = git_dir(tempdir_factory) +def test_get_root_deeper(in_git_dir): + expected = os.path.normcase(in_git_dir.strpath) + with in_git_dir.join('foo').ensure_dir().as_cwd(): + assert os.path.normcase(git.get_root()) == expected - foo_path = os.path.join(path, 'foo') - os.mkdir(foo_path) - with cwd(foo_path): - assert os.path.normcase(git.get_root()) == os.path.normcase(path) +def test_get_root_not_git_dir(in_tmpdir): + with pytest.raises(FatalError): + git.get_root() -def test_get_root_not_git_dir(tempdir_factory): - with cwd(tempdir_factory.get()): - with pytest.raises(FatalError): - git.get_root() +def test_get_staged_files_deleted(in_git_dir): + in_git_dir.join('test').ensure() + cmd_output('git', 'add', 'test') + cmd_output('git', 'commit', '-m', 'foo', '--allow-empty') + cmd_output('git', 'rm', '--cached', 'test') + assert git.get_staged_files() == [] -def test_get_staged_files_deleted(tempdir_factory): - path = git_dir(tempdir_factory) - with cwd(path): - open('test', 'a').close() - cmd_output('git', 'add', 'test') - cmd_output('git', 'commit', '-m', 'foo', '--allow-empty') - cmd_output('git', 'rm', '--cached', 'test') - assert git.get_staged_files() == [] - -def test_is_not_in_merge_conflict(tempdir_factory): - path = git_dir(tempdir_factory) - with cwd(path): - assert git.is_in_merge_conflict() is False +def test_is_not_in_merge_conflict(in_git_dir): + assert git.is_in_merge_conflict() is False def test_is_in_merge_conflict(in_merge_conflict): @@ -114,11 +103,10 @@ def test_parse_merge_msg_for_conflicts(input, expected_output): assert ret == expected_output -def test_get_changed_files(in_tmpdir): - cmd_output('git', 'init', '.') +def test_get_changed_files(in_git_dir): cmd_output('git', 'commit', '--allow-empty', '-m', 'initial commit') - open('a.txt', 'a').close() - open('b.txt', 'a').close() + in_git_dir.join('a.txt').ensure() + in_git_dir.join('b.txt').ensure() cmd_output('git', 'add', '.') cmd_output('git', 'commit', '-m', 'add some files') files = git.get_changed_files('HEAD', 'HEAD^') @@ -143,15 +131,12 @@ def test_zsplit(s, expected): @pytest.fixture -def non_ascii_repo(tmpdir): - repo = tmpdir.join('repo').ensure_dir() - with repo.as_cwd(): - cmd_output('git', 'init', '.') - cmd_output('git', 'commit', '--allow-empty', '-m', 'initial commit') - repo.join('интервью').ensure() - cmd_output('git', 'add', '.') - cmd_output('git', 'commit', '--allow-empty', '-m', 'initial commit') - yield repo +def non_ascii_repo(in_git_dir): + cmd_output('git', 'commit', '--allow-empty', '-m', 'initial commit') + in_git_dir.join('интервью').ensure() + cmd_output('git', 'add', '.') + cmd_output('git', 'commit', '--allow-empty', '-m', 'initial commit') + yield in_git_dir def test_all_files_non_ascii(non_ascii_repo): diff --git a/tests/meta_hooks/check_hooks_apply_test.py b/tests/meta_hooks/check_hooks_apply_test.py index d48d9d7af..06bdd0455 100644 --- a/tests/meta_hooks/check_hooks_apply_test.py +++ b/tests/meta_hooks/check_hooks_apply_test.py @@ -1,10 +1,8 @@ from pre_commit.meta_hooks import check_hooks_apply from testing.fixtures import add_config_to_repo -from testing.fixtures import git_dir -from testing.util import cwd -def test_hook_excludes_everything(capsys, tempdir_factory, mock_store_dir): +def test_hook_excludes_everything(capsys, in_git_dir, mock_store_dir): config = { 'repos': [ { @@ -19,17 +17,15 @@ def test_hook_excludes_everything(capsys, tempdir_factory, mock_store_dir): ], } - repo = git_dir(tempdir_factory) - add_config_to_repo(repo, config) + add_config_to_repo(in_git_dir.strpath, config) - with cwd(repo): - assert check_hooks_apply.main(()) == 1 + assert check_hooks_apply.main(()) == 1 out, _ = capsys.readouterr() assert 'check-useless-excludes does not apply to this repository' in out -def test_hook_includes_nothing(capsys, tempdir_factory, mock_store_dir): +def test_hook_includes_nothing(capsys, in_git_dir, mock_store_dir): config = { 'repos': [ { @@ -44,17 +40,15 @@ def test_hook_includes_nothing(capsys, tempdir_factory, mock_store_dir): ], } - repo = git_dir(tempdir_factory) - add_config_to_repo(repo, config) + add_config_to_repo(in_git_dir.strpath, config) - with cwd(repo): - assert check_hooks_apply.main(()) == 1 + assert check_hooks_apply.main(()) == 1 out, _ = capsys.readouterr() assert 'check-useless-excludes does not apply to this repository' in out -def test_hook_types_not_matched(capsys, tempdir_factory, mock_store_dir): +def test_hook_types_not_matched(capsys, in_git_dir, mock_store_dir): config = { 'repos': [ { @@ -69,19 +63,15 @@ def test_hook_types_not_matched(capsys, tempdir_factory, mock_store_dir): ], } - repo = git_dir(tempdir_factory) - add_config_to_repo(repo, config) + add_config_to_repo(in_git_dir.strpath, config) - with cwd(repo): - assert check_hooks_apply.main(()) == 1 + assert check_hooks_apply.main(()) == 1 out, _ = capsys.readouterr() assert 'check-useless-excludes does not apply to this repository' in out -def test_hook_types_excludes_everything( - capsys, tempdir_factory, mock_store_dir, -): +def test_hook_types_excludes_everything(capsys, in_git_dir, mock_store_dir): config = { 'repos': [ { @@ -96,17 +86,15 @@ def test_hook_types_excludes_everything( ], } - repo = git_dir(tempdir_factory) - add_config_to_repo(repo, config) + add_config_to_repo(in_git_dir.strpath, config) - with cwd(repo): - assert check_hooks_apply.main(()) == 1 + assert check_hooks_apply.main(()) == 1 out, _ = capsys.readouterr() assert 'check-useless-excludes does not apply to this repository' in out -def test_valid_exceptions(capsys, tempdir_factory, mock_store_dir): +def test_valid_exceptions(capsys, in_git_dir, mock_store_dir): config = { 'repos': [ { @@ -142,11 +130,9 @@ def test_valid_exceptions(capsys, tempdir_factory, mock_store_dir): ], } - repo = git_dir(tempdir_factory) - add_config_to_repo(repo, config) + add_config_to_repo(in_git_dir.strpath, config) - with cwd(repo): - assert check_hooks_apply.main(()) == 0 + assert check_hooks_apply.main(()) == 0 out, _ = capsys.readouterr() assert out == '' diff --git a/tests/meta_hooks/check_useless_excludes_test.py b/tests/meta_hooks/check_useless_excludes_test.py index b2cc18731..4adaacd38 100644 --- a/tests/meta_hooks/check_useless_excludes_test.py +++ b/tests/meta_hooks/check_useless_excludes_test.py @@ -1,10 +1,8 @@ from pre_commit.meta_hooks import check_useless_excludes from testing.fixtures import add_config_to_repo -from testing.fixtures import git_dir -from testing.util import cwd -def test_useless_exclude_global(capsys, tempdir_factory): +def test_useless_exclude_global(capsys, in_git_dir): config = { 'exclude': 'foo', 'repos': [ @@ -15,18 +13,16 @@ def test_useless_exclude_global(capsys, tempdir_factory): ], } - repo = git_dir(tempdir_factory) - add_config_to_repo(repo, config) + add_config_to_repo(in_git_dir.strpath, config) - with cwd(repo): - assert check_useless_excludes.main(()) == 1 + assert check_useless_excludes.main(()) == 1 out, _ = capsys.readouterr() out = out.strip() assert "The global exclude pattern 'foo' does not match any files" == out -def test_useless_exclude_for_hook(capsys, tempdir_factory): +def test_useless_exclude_for_hook(capsys, in_git_dir): config = { 'repos': [ { @@ -36,11 +32,9 @@ def test_useless_exclude_for_hook(capsys, tempdir_factory): ], } - repo = git_dir(tempdir_factory) - add_config_to_repo(repo, config) + add_config_to_repo(in_git_dir.strpath, config) - with cwd(repo): - assert check_useless_excludes.main(()) == 1 + assert check_useless_excludes.main(()) == 1 out, _ = capsys.readouterr() out = out.strip() @@ -51,7 +45,7 @@ def test_useless_exclude_for_hook(capsys, tempdir_factory): assert expected == out -def test_useless_exclude_with_types_filter(capsys, tempdir_factory): +def test_useless_exclude_with_types_filter(capsys, in_git_dir): config = { 'repos': [ { @@ -67,11 +61,9 @@ def test_useless_exclude_with_types_filter(capsys, tempdir_factory): ], } - repo = git_dir(tempdir_factory) - add_config_to_repo(repo, config) + add_config_to_repo(in_git_dir.strpath, config) - with cwd(repo): - assert check_useless_excludes.main(()) == 1 + assert check_useless_excludes.main(()) == 1 out, _ = capsys.readouterr() out = out.strip() @@ -82,7 +74,7 @@ def test_useless_exclude_with_types_filter(capsys, tempdir_factory): assert expected == out -def test_no_excludes(capsys, tempdir_factory): +def test_no_excludes(capsys, in_git_dir): config = { 'repos': [ { @@ -92,17 +84,15 @@ def test_no_excludes(capsys, tempdir_factory): ], } - repo = git_dir(tempdir_factory) - add_config_to_repo(repo, config) + add_config_to_repo(in_git_dir.strpath, config) - with cwd(repo): - assert check_useless_excludes.main(()) == 0 + assert check_useless_excludes.main(()) == 0 out, _ = capsys.readouterr() assert out == '' -def test_valid_exclude(capsys, tempdir_factory): +def test_valid_exclude(capsys, in_git_dir): config = { 'repos': [ { @@ -117,11 +107,9 @@ def test_valid_exclude(capsys, tempdir_factory): ], } - repo = git_dir(tempdir_factory) - add_config_to_repo(repo, config) + add_config_to_repo(in_git_dir.strpath, config) - with cwd(repo): - assert check_useless_excludes.main(()) == 0 + assert check_useless_excludes.main(()) == 0 out, _ = capsys.readouterr() assert out == '' diff --git a/tests/repository_test.py b/tests/repository_test.py index 929640372..606bfe759 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals import collections -import io import os.path import re import shutil @@ -24,7 +23,6 @@ from pre_commit.repository import Repository from pre_commit.util import cmd_output from testing.fixtures import config_with_local_hooks -from testing.fixtures import git_dir from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo from testing.fixtures import modify_manifest @@ -96,17 +94,14 @@ def test_python_hook_args_with_spaces(tempdir_factory, store): ) -def test_python_hook_weird_setup_cfg(tempdir_factory, store): - path = git_dir(tempdir_factory) - with cwd(path): - with io.open('setup.cfg', 'w') as setup_cfg: - setup_cfg.write('[install]\ninstall_scripts=/usr/sbin\n') +def test_python_hook_weird_setup_cfg(in_git_dir, tempdir_factory, store): + in_git_dir.join('setup.cfg').write('[install]\ninstall_scripts=/usr/sbin') - _test_hook_repo( - tempdir_factory, store, 'python_hooks_repo', - 'foo', [os.devnull], - b"['" + five.to_bytes(os.devnull) + b"']\nHello World\n", - ) + _test_hook_repo( + tempdir_factory, store, 'python_hooks_repo', + 'foo', [os.devnull], + b"['" + five.to_bytes(os.devnull) + b"']\nHello World\n", + ) @xfailif_no_venv @@ -444,14 +439,12 @@ def _norm_pwd(path): )[1].strip() -def test_cwd_of_hook(tempdir_factory, store): +def test_cwd_of_hook(in_git_dir, tempdir_factory, store): # Note: this doubles as a test for `system` hooks - path = git_dir(tempdir_factory) - with cwd(path): - _test_hook_repo( - tempdir_factory, store, 'prints_cwd_repo', - 'prints_cwd', ['-L'], _norm_pwd(path) + b'\n', - ) + _test_hook_repo( + tempdir_factory, store, 'prints_cwd_repo', + 'prints_cwd', ['-L'], _norm_pwd(in_git_dir.strpath) + b'\n', + ) def test_lots_of_files(tempdir_factory, store): diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 73a6b585c..9f226a41a 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -31,14 +31,11 @@ def get_short_git_status(): @pytest.fixture -def foo_staged(tempdir_factory): - path = git_dir(tempdir_factory) - with cwd(path): - with io.open('foo', 'w') as foo_file: - foo_file.write(FOO_CONTENTS) - cmd_output('git', 'add', 'foo') - foo_filename = os.path.join(path, 'foo') - yield auto_namedtuple(path=path, foo_filename=foo_filename) +def foo_staged(in_git_dir): + foo = in_git_dir.join('foo') + foo.write(FOO_CONTENTS) + cmd_output('git', 'add', 'foo') + yield auto_namedtuple(path=in_git_dir.strpath, foo_filename=foo.strpath) def _test_foo_state( @@ -134,13 +131,11 @@ def test_foo_both_modify_conflicting(foo_staged, patch_dir): @pytest.fixture -def img_staged(tempdir_factory): - path = git_dir(tempdir_factory) - with cwd(path): - img_filename = os.path.join(path, 'img.jpg') - shutil.copy(get_resource_path('img1.jpg'), img_filename) - cmd_output('git', 'add', 'img.jpg') - yield auto_namedtuple(path=path, img_filename=img_filename) +def img_staged(in_git_dir): + img = in_git_dir.join('img.jpg') + shutil.copy(get_resource_path('img1.jpg'), img.strpath) + cmd_output('git', 'add', 'img.jpg') + yield auto_namedtuple(path=in_git_dir.strpath, img_filename=img.strpath) def _test_img_state(path, expected_file='img1.jpg', status='A'): From 28c97a95cddf69ae86c47f35b94abcceb82001de Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Fri, 28 Dec 2018 20:06:52 +0000 Subject: [PATCH 311/544] Don't fail if GPG signing is configured by default. All references. --- testing/fixtures.py | 23 ++++++----------------- testing/util.py | 15 +++++++++++++++ tests/commands/autoupdate_test.py | 13 +++++++------ tests/commands/install_uninstall_test.py | 17 +++++++++-------- tests/commands/run_test.py | 4 ++-- tests/conftest.py | 11 ++++++----- tests/git_test.py | 9 +++++---- tests/make_archives_test.py | 5 +++-- tests/staged_files_only_test.py | 7 ++++--- tests/store_test.py | 6 +++--- 10 files changed, 60 insertions(+), 50 deletions(-) diff --git a/testing/fixtures.py b/testing/fixtures.py index 287eb3092..247d2c4c0 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -18,6 +18,7 @@ from pre_commit.clientlib import load_manifest from pre_commit.util import cmd_output from testing.util import get_resource_path +from testing.util import git_commit def copy_tree_to_path(src_dir, dest_dir): @@ -48,7 +49,7 @@ def make_repo(tempdir_factory, repo_source): path = git_dir(tempdir_factory) copy_tree_to_path(get_resource_path(repo_source), path) cmd_output('git', 'add', '.', cwd=path) - cmd_output('git', 'commit', '--no-gpg-sign', '-m', 'Add hooks', cwd=path) + git_commit('Add hooks', cwd=path) return path @@ -63,11 +64,7 @@ def modify_manifest(path): yield manifest with io.open(manifest_path, 'w') as manifest_file: manifest_file.write(ordered_dump(manifest, **C.YAML_DUMP_KWARGS)) - cmd_output( - 'git', 'commit', '--no-gpg-sign', '-am', - 'update {}'.format(C.MANIFEST_FILE), - cwd=path, - ) + git_commit('update {}'.format(C.MANIFEST_FILE), cwd=path) @contextlib.contextmanager @@ -82,9 +79,7 @@ def modify_config(path='.', commit=True): with io.open(config_path, 'w', encoding='UTF-8') as config_file: config_file.write(ordered_dump(config, **C.YAML_DUMP_KWARGS)) if commit: - cmd_output( - 'git', 'commit', '--no-gpg-sign', '-am', 'update config', cwd=path, - ) + git_commit('update config', cwd=path) def config_with_local_hooks(): @@ -140,19 +135,13 @@ def write_config(directory, config, config_file=C.CONFIG_FILE): def add_config_to_repo(git_path, config, config_file=C.CONFIG_FILE): write_config(git_path, config, config_file=config_file) cmd_output('git', 'add', config_file, cwd=git_path) - cmd_output( - 'git', 'commit', '--no-gpg-sign', '-m', 'Add hooks config', - cwd=git_path, - ) + git_commit('Add hooks config', cwd=git_path) return git_path def remove_config_from_repo(git_path, config_file=C.CONFIG_FILE): cmd_output('git', 'rm', config_file, cwd=git_path) - cmd_output( - 'git', 'commit', '--no-gpg-sign', '-m', 'Remove hooks config', - cwd=git_path, - ) + git_commit('Remove hooks config', cwd=git_path) return git_path diff --git a/testing/util.py b/testing/util.py index ed38affeb..0673a2e99 100644 --- a/testing/util.py +++ b/testing/util.py @@ -133,3 +133,18 @@ def cwd(path): yield finally: os.chdir(original_cwd) + + +def git_commit(msg, *_args, **kwargs): + args = ['git'] + config = kwargs.pop('config', None) + if config is not None: + args.extend(['-C', config]) + args.append('commit') + if msg is not None: + args.extend(['-m', msg]) + if '--allow-empty' not in _args: + args.append('--allow-empty') + if '--no-gpg-sign' not in _args: + args.append('--no-gpg-sign') + return cmd_output(*(tuple(args) + tuple(_args)), **kwargs) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 34c7292b0..583cacec5 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -21,6 +21,7 @@ from testing.fixtures import make_repo from testing.fixtures import write_config from testing.util import get_resource_path +from testing.util import git_commit @pytest.fixture @@ -59,11 +60,11 @@ def test_autoupdate_old_revision_broken(tempdir_factory, in_tmpdir, store): config = make_config_from_repo(path, check=False) cmd_output('git', 'mv', C.MANIFEST_FILE, 'nope.yaml', cwd=path) - cmd_output('git', 'commit', '-m', 'simulate old repo', cwd=path) + git_commit('simulate old repo', cwd=path) # Assume this is the revision the user's old repository was at rev = git.head_rev(path) cmd_output('git', 'mv', 'nope.yaml', C.MANIFEST_FILE, cwd=path) - cmd_output('git', 'commit', '-m', 'move hooks file', cwd=path) + git_commit('move hooks file', cwd=path) update_rev = git.head_rev(path) config['rev'] = rev @@ -84,7 +85,7 @@ def out_of_date_repo(tempdir_factory): original_rev = git.head_rev(path) # Make a commit - cmd_output('git', 'commit', '--allow-empty', '-m', 'foo', cwd=path) + git_commit('foo', cwd=path) head_rev = git.head_rev(path) yield auto_namedtuple( @@ -239,7 +240,7 @@ def test_autoupdate_tagged_repo(tagged_repo, in_tmpdir, store): @pytest.fixture def tagged_repo_with_more_commits(tagged_repo): - cmd_output('git', 'commit', '--allow-empty', '-mfoo', cwd=tagged_repo.path) + git_commit('foo', cwd=tagged_repo.path) yield tagged_repo @@ -263,7 +264,7 @@ def test_autoupdate_latest_no_config(out_of_date_repo, in_tmpdir, store): write_config('.', config) cmd_output('git', '-C', out_of_date_repo.path, 'rm', '-r', ':/') - cmd_output('git', '-C', out_of_date_repo.path, 'commit', '-m', 'rm') + git_commit('rm', config=out_of_date_repo.path) ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) assert ret == 1 @@ -281,7 +282,7 @@ def hook_disappearing_repo(tempdir_factory): os.path.join(path, C.MANIFEST_FILE), ) cmd_output('git', 'add', '.', cwd=path) - cmd_output('git', 'commit', '-m', 'Remove foo', cwd=path) + git_commit('Remove foo', cwd=path) yield auto_namedtuple(path=path, original_rev=original_rev) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index ce74a2ea1..3228b8daa 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -28,6 +28,7 @@ from testing.fixtures import remove_config_from_repo from testing.util import cmd_output_mocked_pre_commit_home from testing.util import cwd +from testing.util import git_commit from testing.util import xfailif_no_symlink @@ -109,7 +110,7 @@ def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): open(touch_file, 'a').close() cmd_output('git', 'add', touch_file) return cmd_output_mocked_pre_commit_home( - 'git', 'commit', '-am', commit_msg, '--allow-empty', + 'git', 'commit', '-am', commit_msg, '--allow-empty', '--no-gpg-sign', # git commit puts pre-commit to stderr stderr=subprocess.STDOUT, retcode=None, @@ -151,7 +152,7 @@ def test_install_pre_commit_and_run_custom_path(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): cmd_output('git', 'mv', C.CONFIG_FILE, 'custom-config.yaml') - cmd_output('git', 'commit', '-m', 'move pre-commit config') + git_commit('move pre-commit config') assert install('custom-config.yaml', store) == 0 ret, output = _get_commit_output(tempdir_factory) @@ -163,7 +164,7 @@ def test_install_in_submodule_and_run(tempdir_factory, store): src_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') parent_path = git_dir(tempdir_factory) cmd_output('git', 'submodule', 'add', src_path, 'sub', cwd=parent_path) - cmd_output('git', 'commit', '-m', 'foo', cwd=parent_path) + git_commit('foo', cwd=parent_path) sub_pth = os.path.join(parent_path, 'sub') with cwd(sub_pth): @@ -193,7 +194,7 @@ def test_commit_am(tempdir_factory, store): # Make an unstaged change open('unstaged', 'w').close() cmd_output('git', 'add', '.') - cmd_output('git', 'commit', '-m', 'foo') + git_commit('foo') with io.open('unstaged', 'w') as foo_file: foo_file.write('Oh hai') @@ -208,12 +209,12 @@ def test_unicode_merge_commit_message(tempdir_factory, store): with cwd(path): assert install(C.CONFIG_FILE, store) == 0 cmd_output('git', 'checkout', 'master', '-b', 'foo') - cmd_output('git', 'commit', '--allow-empty', '-n', '-m', 'branch2') + git_commit('branch2', '-n') cmd_output('git', 'checkout', 'master') cmd_output('git', 'merge', 'foo', '--no-ff', '--no-commit', '-m', '☃') # Used to crash cmd_output_mocked_pre_commit_home( - 'git', 'commit', '--no-edit', + 'git', 'commit', '--no-edit', '--no-gpg-sign', tempdir_factory=tempdir_factory, ) @@ -246,8 +247,8 @@ def test_environment_not_sourced(tempdir_factory, store): # Use a specific homedir to ignore --user installs homedir = tempdir_factory.get() - ret, stdout, stderr = cmd_output( - 'git', 'commit', '--allow-empty', '-m', 'foo', + ret, stdout, stderr = git_commit( + 'foo', env={ 'HOME': homedir, 'PATH': _path_without_us(), diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 33920e5e7..6d9a95927 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -479,7 +479,7 @@ def test_stdout_write_bug_py26(repo_with_failing_hook, store, tempdir_factory): # Have to use subprocess because pytest monkeypatches sys.stdout _, stdout, _ = cmd_output_mocked_pre_commit_home( - 'git', 'commit', '-m', 'Commit!', + 'git', 'commit', '-m', 'Commit!', '--no-gpg-sign', # git commit puts pre-commit to stderr stderr=subprocess.STDOUT, retcode=None, @@ -508,7 +508,7 @@ def test_lots_of_files(store, tempdir_factory): install(C.CONFIG_FILE, store) cmd_output_mocked_pre_commit_home( - 'git', 'commit', '-m', 'Commit!', + 'git', 'commit', '-m', 'Commit!', '--no-gpg-sign', # git commit puts pre-commit to stderr stderr=subprocess.STDOUT, tempdir_factory=tempdir_factory, diff --git a/tests/conftest.py b/tests/conftest.py index 49fbf3fc1..a4e3d991c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,6 +19,7 @@ from testing.fixtures import make_consuming_repo from testing.fixtures import write_config from testing.util import cwd +from testing.util import git_commit @pytest.fixture(autouse=True) @@ -79,7 +80,7 @@ def _make_conflict(): with io.open('foo_only_file', 'w') as foo_only_file: foo_only_file.write('foo') cmd_output('git', 'add', 'foo_only_file') - cmd_output('git', 'commit', '-m', 'conflict_file') + git_commit('conflict_file') cmd_output('git', 'checkout', 'origin/master', '-b', 'bar') with io.open('conflict_file', 'w') as conflict_file: conflict_file.write('harp\nddrp\n') @@ -87,7 +88,7 @@ def _make_conflict(): with io.open('bar_only_file', 'w') as bar_only_file: bar_only_file.write('bar') cmd_output('git', 'add', 'bar_only_file') - cmd_output('git', 'commit', '-m', 'conflict_file') + git_commit('conflict_file') cmd_output('git', 'merge', 'foo', retcode=None) @@ -96,7 +97,7 @@ def in_merge_conflict(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') open(os.path.join(path, 'dummy'), 'a').close() cmd_output('git', 'add', 'dummy', cwd=path) - cmd_output('git', 'commit', '-m', 'Add config.', cwd=path) + git_commit('Add config.', cwd=path) conflict_path = tempdir_factory.get() cmd_output('git', 'clone', path, conflict_path) @@ -109,7 +110,7 @@ def in_merge_conflict(tempdir_factory): def in_conflicting_submodule(tempdir_factory): git_dir_1 = git_dir(tempdir_factory) git_dir_2 = git_dir(tempdir_factory) - cmd_output('git', 'commit', '--allow-empty', '-minit!', cwd=git_dir_2) + git_commit('init!', cwd=git_dir_2) cmd_output('git', 'submodule', 'add', git_dir_2, 'sub', cwd=git_dir_1) with cwd(os.path.join(git_dir_1, 'sub')): _make_conflict() @@ -135,7 +136,7 @@ def commit_msg_repo(tempdir_factory): write_config(path, config) with cwd(path): cmd_output('git', 'add', '.') - cmd_output('git', 'commit', '-m', 'add hooks') + git_commit('add hooks') yield path diff --git a/tests/git_test.py b/tests/git_test.py index 2a9bda4a2..ebc7d16cd 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -9,6 +9,7 @@ from pre_commit import git from pre_commit.error_handler import FatalError from pre_commit.util import cmd_output +from testing.util import git_commit def test_get_root_at_root(in_git_dir): @@ -104,11 +105,11 @@ def test_parse_merge_msg_for_conflicts(input, expected_output): def test_get_changed_files(in_git_dir): - cmd_output('git', 'commit', '--allow-empty', '-m', 'initial commit') + git_commit('initial commit') in_git_dir.join('a.txt').ensure() in_git_dir.join('b.txt').ensure() cmd_output('git', 'add', '.') - cmd_output('git', 'commit', '-m', 'add some files') + git_commit('add some files') files = git.get_changed_files('HEAD', 'HEAD^') assert files == ['a.txt', 'b.txt'] @@ -132,10 +133,10 @@ def test_zsplit(s, expected): @pytest.fixture def non_ascii_repo(in_git_dir): - cmd_output('git', 'commit', '--allow-empty', '-m', 'initial commit') + git_commit('initial commit') in_git_dir.join('интервью').ensure() cmd_output('git', 'add', '.') - cmd_output('git', 'commit', '--allow-empty', '-m', 'initial commit') + git_commit('initial commit') yield in_git_dir diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py index 7f1983222..287ac252d 100644 --- a/tests/make_archives_test.py +++ b/tests/make_archives_test.py @@ -8,6 +8,7 @@ from pre_commit import make_archives from pre_commit.util import cmd_output from testing.fixtures import git_dir +from testing.util import git_commit def test_make_archive(tempdir_factory): @@ -16,13 +17,13 @@ def test_make_archive(tempdir_factory): # Add a files to the git directory open(os.path.join(git_path, 'foo'), 'a').close() cmd_output('git', 'add', '.', cwd=git_path) - cmd_output('git', 'commit', '-m', 'foo', cwd=git_path) + git_commit('foo', cwd=git_path) # We'll use this rev head_rev = git.head_rev(git_path) # And check that this file doesn't exist open(os.path.join(git_path, 'bar'), 'a').close() cmd_output('git', 'add', '.', cwd=git_path) - cmd_output('git', 'commit', '-m', 'bar', cwd=git_path) + git_commit('bar', cwd=git_path) # Do the thing archive_path = make_archives.make_archive( diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 9f226a41a..4e7cd9b14 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -15,6 +15,7 @@ from testing.fixtures import git_dir from testing.util import cwd from testing.util import get_resource_path +from testing.util import git_commit FOO_CONTENTS = '\n'.join(('1', '2', '3', '4', '5', '6', '7', '8', '')) @@ -186,9 +187,9 @@ def test_img_conflict(img_staged, patch_dir): def submodule_with_commits(tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): - cmd_output('git', 'commit', '--allow-empty', '-m', 'foo') + git_commit('foo') rev1 = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() - cmd_output('git', 'commit', '--allow-empty', '-m', 'bar') + git_commit('bar') rev2 = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() yield auto_namedtuple(path=path, rev1=rev1, rev2=rev2) @@ -331,7 +332,7 @@ def test_autocrlf_commited_crlf(in_git_dir, patch_dir): cmd_output('git', 'config', '--local', 'core.autocrlf', 'false') _write(b'1\r\n2\r\n') cmd_output('git', 'add', 'foo') - cmd_output('git', 'commit', '-m', 'Check in crlf') + git_commit('Check in crlf') cmd_output('git', 'config', '--local', 'core.autocrlf', 'true') _write(b'1\r\n2\r\n\r\n\r\n\r\n') diff --git a/tests/store_test.py b/tests/store_test.py index bed0e9017..e22c3aee9 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -12,10 +12,10 @@ from pre_commit import git from pre_commit.store import _get_default_directory from pre_commit.store import Store -from pre_commit.util import cmd_output from pre_commit.util import rmtree from testing.fixtures import git_dir from testing.util import cwd +from testing.util import git_commit def test_our_session_fixture_works(): @@ -90,9 +90,9 @@ def test_does_not_recreate_if_directory_already_exists(store): def test_clone(store, tempdir_factory, log_info_mock): path = git_dir(tempdir_factory) with cwd(path): - cmd_output('git', 'commit', '--allow-empty', '-m', 'foo') + git_commit('foo') rev = git.head_rev(path) - cmd_output('git', 'commit', '--allow-empty', '-m', 'bar') + git_commit('bar') ret = store.clone(path, rev) # Should have printed some stuff From 160a11a0a71aa530987ae8bfc4f62603956316d2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 28 Dec 2018 15:13:06 -0800 Subject: [PATCH 312/544] Improve git_commit helper --- testing/fixtures.py | 10 +++--- testing/util.py | 21 +++++------- tests/commands/autoupdate_test.py | 15 ++++----- tests/commands/install_uninstall_test.py | 32 +++++++++--------- tests/commands/run_test.py | 9 ++--- tests/conftest.py | 10 +++--- tests/git_test.py | 10 +++--- tests/make_archives_test.py | 42 +++++++++++------------- tests/staged_files_only_test.py | 6 ++-- tests/store_test.py | 4 +-- 10 files changed, 75 insertions(+), 84 deletions(-) diff --git a/testing/fixtures.py b/testing/fixtures.py index 247d2c4c0..91c095a8c 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -49,7 +49,7 @@ def make_repo(tempdir_factory, repo_source): path = git_dir(tempdir_factory) copy_tree_to_path(get_resource_path(repo_source), path) cmd_output('git', 'add', '.', cwd=path) - git_commit('Add hooks', cwd=path) + git_commit(msg=make_repo.__name__, cwd=path) return path @@ -64,7 +64,7 @@ def modify_manifest(path): yield manifest with io.open(manifest_path, 'w') as manifest_file: manifest_file.write(ordered_dump(manifest, **C.YAML_DUMP_KWARGS)) - git_commit('update {}'.format(C.MANIFEST_FILE), cwd=path) + git_commit(msg=modify_manifest.__name__, cwd=path) @contextlib.contextmanager @@ -79,7 +79,7 @@ def modify_config(path='.', commit=True): with io.open(config_path, 'w', encoding='UTF-8') as config_file: config_file.write(ordered_dump(config, **C.YAML_DUMP_KWARGS)) if commit: - git_commit('update config', cwd=path) + git_commit(msg=modify_config.__name__, cwd=path) def config_with_local_hooks(): @@ -135,13 +135,13 @@ def write_config(directory, config, config_file=C.CONFIG_FILE): def add_config_to_repo(git_path, config, config_file=C.CONFIG_FILE): write_config(git_path, config, config_file=config_file) cmd_output('git', 'add', config_file, cwd=git_path) - git_commit('Add hooks config', cwd=git_path) + git_commit(msg=add_config_to_repo.__name__, cwd=git_path) return git_path def remove_config_from_repo(git_path, config_file=C.CONFIG_FILE): cmd_output('git', 'rm', config_file, cwd=git_path) - git_commit('Remove hooks config', cwd=git_path) + git_commit(msg=remove_config_from_repo.__name__, cwd=git_path) return git_path diff --git a/testing/util.py b/testing/util.py index 0673a2e99..f0406089b 100644 --- a/testing/util.py +++ b/testing/util.py @@ -135,16 +135,11 @@ def cwd(path): os.chdir(original_cwd) -def git_commit(msg, *_args, **kwargs): - args = ['git'] - config = kwargs.pop('config', None) - if config is not None: - args.extend(['-C', config]) - args.append('commit') - if msg is not None: - args.extend(['-m', msg]) - if '--allow-empty' not in _args: - args.append('--allow-empty') - if '--no-gpg-sign' not in _args: - args.append('--no-gpg-sign') - return cmd_output(*(tuple(args) + tuple(_args)), **kwargs) +def git_commit(*args, **kwargs): + fn = kwargs.pop('fn', cmd_output) + msg = kwargs.pop('msg', 'commit!') + + cmd = ('git', 'commit', '--allow-empty', '--no-gpg-sign', '-a') + args + if msg is not None: # allow skipping `-m` with `msg=None` + cmd += ('-m', msg) + return fn(*cmd, **kwargs) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 583cacec5..e4d3cc881 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -60,11 +60,11 @@ def test_autoupdate_old_revision_broken(tempdir_factory, in_tmpdir, store): config = make_config_from_repo(path, check=False) cmd_output('git', 'mv', C.MANIFEST_FILE, 'nope.yaml', cwd=path) - git_commit('simulate old repo', cwd=path) + git_commit(cwd=path) # Assume this is the revision the user's old repository was at rev = git.head_rev(path) cmd_output('git', 'mv', 'nope.yaml', C.MANIFEST_FILE, cwd=path) - git_commit('move hooks file', cwd=path) + git_commit(cwd=path) update_rev = git.head_rev(path) config['rev'] = rev @@ -84,8 +84,7 @@ def out_of_date_repo(tempdir_factory): path = make_repo(tempdir_factory, 'python_hooks_repo') original_rev = git.head_rev(path) - # Make a commit - git_commit('foo', cwd=path) + git_commit(cwd=path) head_rev = git.head_rev(path) yield auto_namedtuple( @@ -240,7 +239,7 @@ def test_autoupdate_tagged_repo(tagged_repo, in_tmpdir, store): @pytest.fixture def tagged_repo_with_more_commits(tagged_repo): - git_commit('foo', cwd=tagged_repo.path) + git_commit(cwd=tagged_repo.path) yield tagged_repo @@ -263,8 +262,8 @@ def test_autoupdate_latest_no_config(out_of_date_repo, in_tmpdir, store): ) write_config('.', config) - cmd_output('git', '-C', out_of_date_repo.path, 'rm', '-r', ':/') - git_commit('rm', config=out_of_date_repo.path) + cmd_output('git', 'rm', '-r', ':/', cwd=out_of_date_repo.path) + git_commit(cwd=out_of_date_repo.path) ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) assert ret == 1 @@ -282,7 +281,7 @@ def hook_disappearing_repo(tempdir_factory): os.path.join(path, C.MANIFEST_FILE), ) cmd_output('git', 'add', '.', cwd=path) - git_commit('Remove foo', cwd=path) + git_commit(cwd=path) yield auto_namedtuple(path=path, original_rev=original_rev) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 3228b8daa..2faa19178 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -106,11 +106,10 @@ def test_uninstall(in_git_dir, store): def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): - commit_msg = kwargs.pop('commit_msg', 'Commit!') open(touch_file, 'a').close() cmd_output('git', 'add', touch_file) - return cmd_output_mocked_pre_commit_home( - 'git', 'commit', '-am', commit_msg, '--allow-empty', '--no-gpg-sign', + return git_commit( + fn=cmd_output_mocked_pre_commit_home, # git commit puts pre-commit to stderr stderr=subprocess.STDOUT, retcode=None, @@ -132,7 +131,7 @@ def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): NORMAL_PRE_COMMIT_RUN = re.compile( r'^\[INFO\] Initializing environment for .+\.\r?\n' r'Bash hook\.+Passed\r?\n' - r'\[master [a-f0-9]{7}\] Commit!\r?\n' + + r'\[master [a-f0-9]{7}\] commit!\r?\n' + FILES_CHANGED + r' create mode 100644 foo\r?\n$', ) @@ -152,7 +151,7 @@ def test_install_pre_commit_and_run_custom_path(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): cmd_output('git', 'mv', C.CONFIG_FILE, 'custom-config.yaml') - git_commit('move pre-commit config') + git_commit(cwd=path) assert install('custom-config.yaml', store) == 0 ret, output = _get_commit_output(tempdir_factory) @@ -164,7 +163,7 @@ def test_install_in_submodule_and_run(tempdir_factory, store): src_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') parent_path = git_dir(tempdir_factory) cmd_output('git', 'submodule', 'add', src_path, 'sub', cwd=parent_path) - git_commit('foo', cwd=parent_path) + git_commit(cwd=parent_path) sub_pth = os.path.join(parent_path, 'sub') with cwd(sub_pth): @@ -194,7 +193,7 @@ def test_commit_am(tempdir_factory, store): # Make an unstaged change open('unstaged', 'w').close() cmd_output('git', 'add', '.') - git_commit('foo') + git_commit(cwd=path) with io.open('unstaged', 'w') as foo_file: foo_file.write('Oh hai') @@ -209,12 +208,14 @@ def test_unicode_merge_commit_message(tempdir_factory, store): with cwd(path): assert install(C.CONFIG_FILE, store) == 0 cmd_output('git', 'checkout', 'master', '-b', 'foo') - git_commit('branch2', '-n') + git_commit('-n', cwd=path) cmd_output('git', 'checkout', 'master') cmd_output('git', 'merge', 'foo', '--no-ff', '--no-commit', '-m', '☃') # Used to crash - cmd_output_mocked_pre_commit_home( - 'git', 'commit', '--no-edit', '--no-gpg-sign', + git_commit( + '--no-edit', + msg=None, + fn=cmd_output_mocked_pre_commit_home, tempdir_factory=tempdir_factory, ) @@ -248,7 +249,6 @@ def test_environment_not_sourced(tempdir_factory, store): # Use a specific homedir to ignore --user installs homedir = tempdir_factory.get() ret, stdout, stderr = git_commit( - 'foo', env={ 'HOME': homedir, 'PATH': _path_without_us(), @@ -291,7 +291,7 @@ def test_failing_hooks_returns_nonzero(tempdir_factory, store): EXISTING_COMMIT_RUN = re.compile( r'^legacy hook\r?\n' - r'\[master [a-f0-9]{7}\] Commit!\r?\n' + + r'\[master [a-f0-9]{7}\] commit!\r?\n' + FILES_CHANGED + r' create mode 100644 baz\r?\n$', ) @@ -434,7 +434,7 @@ def test_uninstall_doesnt_remove_not_our_hooks(in_git_dir): PRE_INSTALLED = re.compile( r'Bash hook\.+Passed\r?\n' - r'\[master [a-f0-9]{7}\] Commit!\r?\n' + + r'\[master [a-f0-9]{7}\] commit!\r?\n' + FILES_CHANGED + r' create mode 100644 foo\r?\n$', ) @@ -546,7 +546,7 @@ def test_pre_push_force_push_without_fetch(tempdir_factory, store): with cwd(path2): install(C.CONFIG_FILE, store, hook_type='pre-push') - assert _get_commit_output(tempdir_factory, commit_msg='force!')[0] == 0 + assert _get_commit_output(tempdir_factory, msg='force!')[0] == 0 retc, output = _get_push_output(tempdir_factory, opts=('--force',)) assert retc == 0 @@ -625,7 +625,7 @@ def test_commit_msg_integration_passing( ): install(C.CONFIG_FILE, store, hook_type='commit-msg') msg = 'Hi\nSigned off by: me, lol' - retc, out = _get_commit_output(tempdir_factory, commit_msg=msg) + retc, out = _get_commit_output(tempdir_factory, msg=msg) assert retc == 0 first_line = out.splitlines()[0] assert first_line.startswith('Must have "Signed off by:"...') @@ -647,7 +647,7 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): install(C.CONFIG_FILE, store, hook_type='commit-msg') msg = 'Hi\nSigned off by: asottile' - retc, out = _get_commit_output(tempdir_factory, commit_msg=msg) + retc, out = _get_commit_output(tempdir_factory, msg=msg) assert retc == 0 first_line, second_line = out.splitlines()[:2] assert first_line == 'legacy' diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 6d9a95927..28b6ab375 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -24,6 +24,7 @@ from testing.fixtures import read_config from testing.util import cmd_output_mocked_pre_commit_home from testing.util import cwd +from testing.util import git_commit from testing.util import run_opts from testing.util import xfailif_no_symlink @@ -478,8 +479,8 @@ def test_stdout_write_bug_py26(repo_with_failing_hook, store, tempdir_factory): install(C.CONFIG_FILE, store) # Have to use subprocess because pytest monkeypatches sys.stdout - _, stdout, _ = cmd_output_mocked_pre_commit_home( - 'git', 'commit', '-m', 'Commit!', '--no-gpg-sign', + _, stdout, _ = git_commit( + fn=cmd_output_mocked_pre_commit_home, # git commit puts pre-commit to stderr stderr=subprocess.STDOUT, retcode=None, @@ -507,8 +508,8 @@ def test_lots_of_files(store, tempdir_factory): cmd_output('git', 'add', '.') install(C.CONFIG_FILE, store) - cmd_output_mocked_pre_commit_home( - 'git', 'commit', '-m', 'Commit!', '--no-gpg-sign', + git_commit( + fn=cmd_output_mocked_pre_commit_home, # git commit puts pre-commit to stderr stderr=subprocess.STDOUT, tempdir_factory=tempdir_factory, diff --git a/tests/conftest.py b/tests/conftest.py index a4e3d991c..f72af094c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -80,7 +80,7 @@ def _make_conflict(): with io.open('foo_only_file', 'w') as foo_only_file: foo_only_file.write('foo') cmd_output('git', 'add', 'foo_only_file') - git_commit('conflict_file') + git_commit(msg=_make_conflict.__name__) cmd_output('git', 'checkout', 'origin/master', '-b', 'bar') with io.open('conflict_file', 'w') as conflict_file: conflict_file.write('harp\nddrp\n') @@ -88,7 +88,7 @@ def _make_conflict(): with io.open('bar_only_file', 'w') as bar_only_file: bar_only_file.write('bar') cmd_output('git', 'add', 'bar_only_file') - git_commit('conflict_file') + git_commit(msg=_make_conflict.__name__) cmd_output('git', 'merge', 'foo', retcode=None) @@ -97,7 +97,7 @@ def in_merge_conflict(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') open(os.path.join(path, 'dummy'), 'a').close() cmd_output('git', 'add', 'dummy', cwd=path) - git_commit('Add config.', cwd=path) + git_commit(msg=in_merge_conflict.__name__, cwd=path) conflict_path = tempdir_factory.get() cmd_output('git', 'clone', path, conflict_path) @@ -110,7 +110,7 @@ def in_merge_conflict(tempdir_factory): def in_conflicting_submodule(tempdir_factory): git_dir_1 = git_dir(tempdir_factory) git_dir_2 = git_dir(tempdir_factory) - git_commit('init!', cwd=git_dir_2) + git_commit(msg=in_conflicting_submodule.__name__, cwd=git_dir_2) cmd_output('git', 'submodule', 'add', git_dir_2, 'sub', cwd=git_dir_1) with cwd(os.path.join(git_dir_1, 'sub')): _make_conflict() @@ -136,7 +136,7 @@ def commit_msg_repo(tempdir_factory): write_config(path, config) with cwd(path): cmd_output('git', 'add', '.') - git_commit('add hooks') + git_commit(msg=commit_msg_repo.__name__) yield path diff --git a/tests/git_test.py b/tests/git_test.py index ebc7d16cd..cb8a2bf1a 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -31,7 +31,7 @@ def test_get_root_not_git_dir(in_tmpdir): def test_get_staged_files_deleted(in_git_dir): in_git_dir.join('test').ensure() cmd_output('git', 'add', 'test') - cmd_output('git', 'commit', '-m', 'foo', '--allow-empty') + git_commit() cmd_output('git', 'rm', '--cached', 'test') assert git.get_staged_files() == [] @@ -105,11 +105,11 @@ def test_parse_merge_msg_for_conflicts(input, expected_output): def test_get_changed_files(in_git_dir): - git_commit('initial commit') + git_commit() in_git_dir.join('a.txt').ensure() in_git_dir.join('b.txt').ensure() cmd_output('git', 'add', '.') - git_commit('add some files') + git_commit() files = git.get_changed_files('HEAD', 'HEAD^') assert files == ['a.txt', 'b.txt'] @@ -133,10 +133,10 @@ def test_zsplit(s, expected): @pytest.fixture def non_ascii_repo(in_git_dir): - git_commit('initial commit') + git_commit() in_git_dir.join('интервью').ensure() cmd_output('git', 'add', '.') - git_commit('initial commit') + git_commit() yield in_git_dir diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py index 287ac252d..52c9c9b6f 100644 --- a/tests/make_archives_test.py +++ b/tests/make_archives_test.py @@ -1,49 +1,45 @@ from __future__ import absolute_import from __future__ import unicode_literals -import os.path import tarfile from pre_commit import git from pre_commit import make_archives from pre_commit.util import cmd_output -from testing.fixtures import git_dir from testing.util import git_commit -def test_make_archive(tempdir_factory): - output_dir = tempdir_factory.get() - git_path = git_dir(tempdir_factory) +def test_make_archive(in_git_dir, tmpdir): + output_dir = tmpdir.join('output').ensure_dir() # Add a files to the git directory - open(os.path.join(git_path, 'foo'), 'a').close() - cmd_output('git', 'add', '.', cwd=git_path) - git_commit('foo', cwd=git_path) + in_git_dir.join('foo').ensure() + cmd_output('git', 'add', '.') + git_commit() # We'll use this rev - head_rev = git.head_rev(git_path) + head_rev = git.head_rev('.') # And check that this file doesn't exist - open(os.path.join(git_path, 'bar'), 'a').close() - cmd_output('git', 'add', '.', cwd=git_path) - git_commit('bar', cwd=git_path) + in_git_dir.join('bar').ensure() + cmd_output('git', 'add', '.') + git_commit() # Do the thing archive_path = make_archives.make_archive( - 'foo', git_path, head_rev, output_dir, + 'foo', in_git_dir.strpath, head_rev, output_dir.strpath, ) - assert archive_path == os.path.join(output_dir, 'foo.tar.gz') - assert os.path.exists(archive_path) + expected = output_dir.join('foo.tar.gz') + assert archive_path == expected.strpath + assert expected.exists() - extract_dir = tempdir_factory.get() - - # Extract the tar + extract_dir = tmpdir.join('extract').ensure_dir() with tarfile.open(archive_path) as tf: - tf.extractall(extract_dir) + tf.extractall(extract_dir.strpath) # Verify the contents of the tar - assert os.path.exists(os.path.join(extract_dir, 'foo')) - assert os.path.exists(os.path.join(extract_dir, 'foo', 'foo')) - assert not os.path.exists(os.path.join(extract_dir, 'foo', '.git')) - assert not os.path.exists(os.path.join(extract_dir, 'foo', 'bar')) + assert extract_dir.join('foo').isdir() + assert extract_dir.join('foo/foo').exists() + assert not extract_dir.join('foo/.git').exists() + assert not extract_dir.join('foo/bar').exists() def test_main(tmpdir): diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 4e7cd9b14..619d739b1 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -187,9 +187,9 @@ def test_img_conflict(img_staged, patch_dir): def submodule_with_commits(tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): - git_commit('foo') + git_commit() rev1 = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() - git_commit('bar') + git_commit() rev2 = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() yield auto_namedtuple(path=path, rev1=rev1, rev2=rev2) @@ -332,7 +332,7 @@ def test_autocrlf_commited_crlf(in_git_dir, patch_dir): cmd_output('git', 'config', '--local', 'core.autocrlf', 'false') _write(b'1\r\n2\r\n') cmd_output('git', 'add', 'foo') - git_commit('Check in crlf') + git_commit() cmd_output('git', 'config', '--local', 'core.autocrlf', 'true') _write(b'1\r\n2\r\n\r\n\r\n\r\n') diff --git a/tests/store_test.py b/tests/store_test.py index e22c3aee9..8ef10a932 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -90,9 +90,9 @@ def test_does_not_recreate_if_directory_already_exists(store): def test_clone(store, tempdir_factory, log_info_mock): path = git_dir(tempdir_factory) with cwd(path): - git_commit('foo') + git_commit() rev = git.head_rev(path) - git_commit('bar') + git_commit() ret = store.clone(path, rev) # Should have printed some stuff From a49a34ef3d991e95fdda1e986a0e5e9fc826ef87 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Dec 2018 13:13:31 -0800 Subject: [PATCH 313/544] Add identity meta hook --- pre_commit/meta_hooks/identity.py | 13 +++++++++++++ pre_commit/repository.py | 8 ++++++++ tests/meta_hooks/identity_test.py | 6 ++++++ 3 files changed, 27 insertions(+) create mode 100644 pre_commit/meta_hooks/identity.py create mode 100644 tests/meta_hooks/identity_test.py diff --git a/pre_commit/meta_hooks/identity.py b/pre_commit/meta_hooks/identity.py new file mode 100644 index 000000000..ae7377b80 --- /dev/null +++ b/pre_commit/meta_hooks/identity.py @@ -0,0 +1,13 @@ +import sys + +from pre_commit import output + + +def main(argv=None): + argv = argv if argv is not None else sys.argv[1:] + for arg in argv: + output.write_line(arg) + + +if __name__ == '__main__': + exit(main()) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 2a4355069..e245a1a3d 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -237,6 +237,7 @@ def manifest_hooks(self): # The hooks are imported here to prevent circular imports. from pre_commit.meta_hooks import check_hooks_apply from pre_commit.meta_hooks import check_useless_excludes + from pre_commit.meta_hooks import identity def _make_entry(mod): """the hook `entry` is passed through `shlex.split()` by the @@ -260,6 +261,13 @@ def _make_entry(mod): 'language': 'system', 'entry': _make_entry(check_useless_excludes), }, + { + 'id': 'identity', + 'name': 'identity', + 'language': 'system', + 'verbose': True, + 'entry': _make_entry(identity), + }, ] return { diff --git a/tests/meta_hooks/identity_test.py b/tests/meta_hooks/identity_test.py new file mode 100644 index 000000000..3eff00be3 --- /dev/null +++ b/tests/meta_hooks/identity_test.py @@ -0,0 +1,6 @@ +from pre_commit.meta_hooks import identity + + +def test_identity(cap_out): + assert not identity.main(('a', 'b', 'c')) + assert cap_out.get() == 'a\nb\nc\n' From c577ed92e7482094614a64c8b692daebea3a1a15 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 30 Dec 2018 18:11:28 -0800 Subject: [PATCH 314/544] Refactor pre_commit.repository and factor out cached-property --- .pre-commit-config.yaml | 4 + .travis.yml | 5 +- pre_commit/commands/autoupdate.py | 16 +- pre_commit/commands/install_uninstall.py | 6 +- pre_commit/commands/run.py | 69 ++-- pre_commit/meta_hooks/check_hooks_apply.py | 32 +- .../meta_hooks/check_useless_excludes.py | 9 + pre_commit/meta_hooks/helpers.py | 10 + pre_commit/meta_hooks/identity.py | 9 + pre_commit/repository.py | 358 ++++++++---------- setup.py | 1 - .../.pre-commit-hooks.yaml | 2 +- tests/commands/run_test.py | 10 +- tests/repository_test.py | 315 +++++++-------- 14 files changed, 395 insertions(+), 451 deletions(-) create mode 100644 pre_commit/meta_hooks/helpers.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aa237a5eb..4fe852f77 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,6 +19,10 @@ repos: rev: v1.11.2 hooks: - id: validate_manifest +- repo: https://github.com/asottile/pyupgrade + rev: v1.11.0 + hooks: + - id: pyupgrade - repo: https://github.com/asottile/reorder_python_imports rev: v1.3.0 hooks: diff --git a/.travis.yml b/.travis.yml index e59e07179..32376b270 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: python -dist: trusty -sudo: required +dist: xenial services: - docker matrix: @@ -13,8 +12,6 @@ matrix: python: pypy2.7-5.10.0 - env: TOXENV=py37 python: 3.7 - sudo: required - dist: xenial install: pip install coveralls tox script: tox before_install: diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index f40a7c555..f75a19242 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -1,8 +1,8 @@ from __future__ import print_function from __future__ import unicode_literals +import os.path import re -from collections import OrderedDict import six from aspy.yaml import ordered_dump @@ -16,8 +16,8 @@ from pre_commit.clientlib import is_local_repo from pre_commit.clientlib import is_meta_repo from pre_commit.clientlib import load_config +from pre_commit.clientlib import load_manifest from pre_commit.commands.migrate_config import migrate_config -from pre_commit.repository import Repository from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output @@ -52,24 +52,24 @@ def _update_repo(repo_config, store, tags_only): if rev == repo_config['rev']: return repo_config - # Construct a new config with the head rev - new_config = OrderedDict(repo_config) - new_config['rev'] = rev - try: - new_hooks = Repository.create(new_config, store).manifest_hooks + path = store.clone(repo_config['repo'], rev) + manifest = load_manifest(os.path.join(path, C.MANIFEST_FILE)) except InvalidManifestError as e: raise RepositoryCannotBeUpdatedError(six.text_type(e)) # See if any of our hooks were deleted with the new commits hooks = {hook['id'] for hook in repo_config['hooks']} - hooks_missing = hooks - set(new_hooks) + hooks_missing = hooks - {hook['id'] for hook in manifest} if hooks_missing: raise RepositoryCannotBeUpdatedError( 'Cannot update because the tip of master is missing these hooks:\n' '{}'.format(', '.join(sorted(hooks_missing))), ) + # Construct a new config with the head rev + new_config = repo_config.copy() + new_config['rev'] = rev return new_config diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 3e70b4c9f..e27c5b2c7 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -10,7 +10,8 @@ from pre_commit import output from pre_commit.clientlib import load_config from pre_commit.languages import python -from pre_commit.repository import repositories +from pre_commit.repository import all_hooks +from pre_commit.repository import install_hook_envs from pre_commit.util import cmd_output from pre_commit.util import make_executable from pre_commit.util import mkdirp @@ -116,8 +117,7 @@ def install( def install_hooks(config_file, store): - for repository in repositories(load_config(config_file), store): - repository.require_installed() + install_hook_envs(all_hooks(load_config(config_file), store), store) def uninstall(hook_type='pre-commit'): diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index d9280460b..2b90e44e6 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -13,7 +13,8 @@ from pre_commit import output from pre_commit.clientlib import load_config from pre_commit.output import get_hook_message -from pre_commit.repository import repositories +from pre_commit.repository import all_hooks +from pre_commit.repository import install_hook_envs from pre_commit.staged_files_only import staged_files_only from pre_commit.util import cmd_output from pre_commit.util import memoize_by_cwd @@ -32,9 +33,7 @@ def _get_skips(environ): def _hook_msg_start(hook, verbose): - return '{}{}'.format( - '[{}] '.format(hook['id']) if verbose else '', hook['name'], - ) + return '{}{}'.format('[{}] '.format(hook.id) if verbose else '', hook.name) def _filter_by_include_exclude(filenames, include, exclude): @@ -63,21 +62,21 @@ def _filter_by_types(filenames, types, exclude_types): NO_FILES = '(no files to check)' -def _run_single_hook(filenames, hook, repo, args, skips, cols): - include, exclude = hook['files'], hook['exclude'] +def _run_single_hook(filenames, hook, args, skips, cols): + include, exclude = hook.files, hook.exclude filenames = _filter_by_include_exclude(filenames, include, exclude) - types, exclude_types = hook['types'], hook['exclude_types'] + types, exclude_types = hook.types, hook.exclude_types filenames = _filter_by_types(filenames, types, exclude_types) - if hook['language'] == 'pcre': + if hook.language == 'pcre': logger.warning( '`{}` (from {}) uses the deprecated pcre language.\n' 'The pcre language is scheduled for removal in pre-commit 2.x.\n' 'The pygrep language is a more portable (and usually drop-in) ' - 'replacement.'.format(hook['id'], repo.repo_config['repo']), + 'replacement.'.format(hook.id, hook.src), ) - if hook['id'] in skips or hook['alias'] in skips: + if hook.id in skips or hook.alias in skips: output.write(get_hook_message( _hook_msg_start(hook, args.verbose), end_msg=SKIPPED, @@ -86,7 +85,7 @@ def _run_single_hook(filenames, hook, repo, args, skips, cols): cols=cols, )) return 0 - elif not filenames and not hook['always_run']: + elif not filenames and not hook.always_run: output.write(get_hook_message( _hook_msg_start(hook, args.verbose), postfix=NO_FILES, @@ -107,8 +106,8 @@ def _run_single_hook(filenames, hook, repo, args, skips, cols): diff_before = cmd_output( 'git', 'diff', '--no-ext-diff', retcode=None, encoding=None, ) - retcode, stdout, stderr = repo.run_hook( - hook, tuple(filenames) if hook['pass_filenames'] else (), + retcode, stdout, stderr = hook.run( + tuple(filenames) if hook.pass_filenames else (), ) diff_after = cmd_output( 'git', 'diff', '--no-ext-diff', retcode=None, encoding=None, @@ -133,9 +132,9 @@ def _run_single_hook(filenames, hook, repo, args, skips, cols): if ( (stdout or stderr or file_modifications) and - (retcode or args.verbose or hook['verbose']) + (retcode or args.verbose or hook.verbose) ): - output.write_line('hookid: {}\n'.format(hook['id'])) + output.write_line('hookid: {}\n'.format(hook.id)) # Print a message if failing due to file modifications if file_modifications: @@ -149,7 +148,7 @@ def _run_single_hook(filenames, hook, repo, args, skips, cols): for out in (stdout, stderr): assert type(out) is bytes, type(out) if out.strip(): - output.write_line(out.strip(), logfile_name=hook['log_file']) + output.write_line(out.strip(), logfile_name=hook.log_file) output.write_line() return retcode @@ -189,15 +188,15 @@ def _all_filenames(args): return git.get_staged_files() -def _run_hooks(config, repo_hooks, args, environ): +def _run_hooks(config, hooks, args, environ): """Actually run the hooks.""" skips = _get_skips(environ) - cols = _compute_cols([hook for _, hook in repo_hooks], args.verbose) + cols = _compute_cols(hooks, args.verbose) filenames = _all_filenames(args) filenames = _filter_by_include_exclude(filenames, '', config['exclude']) retval = 0 - for repo, hook in repo_hooks: - retval |= _run_single_hook(filenames, hook, repo, args, skips, cols) + for hook in hooks: + retval |= _run_single_hook(filenames, hook, args, skips, cols) if retval and config['fail_fast']: break if ( @@ -252,28 +251,18 @@ def run(config_file, store, args, environ=os.environ): ctx = staged_files_only(store.directory) with ctx: - repo_hooks = [] config = load_config(config_file) - for repo in repositories(config, store): - for _, hook in repo.hooks: - if ( - ( - not args.hook or - hook['id'] == args.hook or - hook['alias'] == args.hook - ) and - ( - not hook['stages'] or - args.hook_stage in hook['stages'] - ) - ): - repo_hooks.append((repo, hook)) - - if args.hook and not repo_hooks: + hooks = [ + hook + for hook in all_hooks(config, store) + if not args.hook or hook.id == args.hook or hook.alias == args.hook + if not hook.stages or args.hook_stage in hook.stages + ] + + if args.hook and not hooks: output.write_line('No hook with id `{}`'.format(args.hook)) return 1 - for repo in {repo for repo, _ in repo_hooks}: - repo.require_installed() + install_hook_envs(hooks, store) - return _run_hooks(config, repo_hooks, args, environ) + return _run_hooks(config, hooks, args, environ) diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index 4c4719c8a..a97830d20 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -5,25 +5,33 @@ from pre_commit.clientlib import load_config from pre_commit.commands.run import _filter_by_include_exclude from pre_commit.commands.run import _filter_by_types -from pre_commit.repository import repositories +from pre_commit.meta_hooks.helpers import make_meta_entry +from pre_commit.repository import all_hooks from pre_commit.store import Store +HOOK_DICT = { + 'id': 'check-hooks-apply', + 'name': 'Check hooks apply to the repository', + 'files': C.CONFIG_FILE, + 'language': 'system', + 'entry': make_meta_entry(__name__), +} + def check_all_hooks_match_files(config_file): files = git.get_all_files() retv = 0 - for repo in repositories(load_config(config_file), Store()): - for hook_id, hook in repo.hooks: - if hook['always_run'] or hook['language'] == 'fail': - continue - include, exclude = hook['files'], hook['exclude'] - filtered = _filter_by_include_exclude(files, include, exclude) - types, exclude_types = hook['types'], hook['exclude_types'] - filtered = _filter_by_types(filtered, types, exclude_types) - if not filtered: - print('{} does not apply to this repository'.format(hook_id)) - retv = 1 + for hook in all_hooks(load_config(config_file), Store()): + if hook.always_run or hook.language == 'fail': + continue + include, exclude = hook.files, hook.exclude + filtered = _filter_by_include_exclude(files, include, exclude) + types, exclude_types = hook.types, hook.exclude_types + filtered = _filter_by_types(filtered, types, exclude_types) + if not filtered: + print('{} does not apply to this repository'.format(hook.id)) + retv = 1 return retv diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 18b9f1637..7918eb31c 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -10,6 +10,15 @@ from pre_commit.clientlib import load_config from pre_commit.clientlib import MANIFEST_HOOK_DICT from pre_commit.commands.run import _filter_by_types +from pre_commit.meta_hooks.helpers import make_meta_entry + +HOOK_DICT = { + 'id': 'check-useless-excludes', + 'name': 'Check for useless excludes', + 'files': C.CONFIG_FILE, + 'language': 'system', + 'entry': make_meta_entry(__name__), +} def exclude_matches_any(filenames, include, exclude): diff --git a/pre_commit/meta_hooks/helpers.py b/pre_commit/meta_hooks/helpers.py new file mode 100644 index 000000000..7ef74861e --- /dev/null +++ b/pre_commit/meta_hooks/helpers.py @@ -0,0 +1,10 @@ +import pipes +import sys + + +def make_meta_entry(modname): + """the hook `entry` is passed through `shlex.split()` by the command + runner, so to prevent issues with spaces and backslashes (on Windows) + it must be quoted here. + """ + return '{} -m {}'.format(pipes.quote(sys.executable), modname) diff --git a/pre_commit/meta_hooks/identity.py b/pre_commit/meta_hooks/identity.py index ae7377b80..0cec32a0c 100644 --- a/pre_commit/meta_hooks/identity.py +++ b/pre_commit/meta_hooks/identity.py @@ -1,6 +1,15 @@ import sys from pre_commit import output +from pre_commit.meta_hooks.helpers import make_meta_entry + +HOOK_DICT = { + 'id': 'identity', + 'name': 'identity', + 'language': 'system', + 'verbose': True, + 'entry': make_meta_entry(__name__), +} def main(argv=None): diff --git a/pre_commit/repository.py b/pre_commit/repository.py index e245a1a3d..c9115dfd9 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -1,20 +1,16 @@ from __future__ import unicode_literals +import collections import io import json import logging import os -import pipes -import shutil -import sys -from cached_property import cached_property from cfgv import apply_defaults from cfgv import validate import pre_commit.constants as C from pre_commit import five -from pre_commit import git from pre_commit.clientlib import is_local_repo from pre_commit.clientlib import is_meta_repo from pre_commit.clientlib import load_manifest @@ -23,6 +19,7 @@ from pre_commit.languages.helpers import environment_dir from pre_commit.prefix import Prefix from pre_commit.util import parse_version +from pre_commit.util import rmtree logger = logging.getLogger('pre_commit') @@ -33,9 +30,7 @@ def _state(additional_deps): def _state_filename(prefix, venv): - return prefix.path( - venv, '.install_state_v' + C.INSTALLED_STATE_VERSION, - ) + return prefix.path(venv, '.install_state_v' + C.INSTALLED_STATE_VERSION) def _read_state(prefix, venv): @@ -44,7 +39,7 @@ def _read_state(prefix, venv): return None else: with io.open(filename) as f: - return json.loads(f.read()) + return json.load(f) def _write_state(prefix, venv, state): @@ -56,53 +51,67 @@ def _write_state(prefix, venv, state): os.rename(staging, state_filename) -def _installed(prefix, language_name, language_version, additional_deps): - language = languages[language_name] - venv = environment_dir(language.ENVIRONMENT_DIR, language_version) - return ( - venv is None or ( - _read_state(prefix, venv) == _state(additional_deps) and - language.healthy(prefix, language_version) - ) - ) +_KEYS = tuple(item.key for item in MANIFEST_HOOK_DICT.items) -def _install_all(venvs, repo_url, store): - """Tuple of (prefix, language, version, deps)""" - def _need_installed(): - return tuple( - (prefix, language_name, version, deps) - for prefix, language_name, version, deps in venvs - if not _installed(prefix, language_name, version, deps) - ) +class Hook(collections.namedtuple('Hook', ('src', 'prefix') + _KEYS)): + __slots__ = () - if not _need_installed(): - return - with store.exclusive_lock(): - # Another process may have already completed this work - need_installed = _need_installed() - if not need_installed: # pragma: no cover (race) - return + @property + def install_key(self): + return ( + self.prefix, + self.language, + self.language_version, + tuple(self.additional_dependencies), + ) - logger.info( - 'Installing environment for {}.'.format(repo_url), + def installed(self): + lang = languages[self.language] + venv = environment_dir(lang.ENVIRONMENT_DIR, self.language_version) + return ( + venv is None or ( + ( + _read_state(self.prefix, venv) == + _state(self.additional_dependencies) + ) and + lang.healthy(self.prefix, self.language_version) + ) ) + + def install(self): + logger.info('Installing environment for {}.'.format(self.src)) logger.info('Once installed this environment will be reused.') logger.info('This may take a few minutes...') - for prefix, language_name, version, deps in need_installed: - language = languages[language_name] - venv = environment_dir(language.ENVIRONMENT_DIR, version) + lang = languages[self.language] + venv = environment_dir(lang.ENVIRONMENT_DIR, self.language_version) - # There's potentially incomplete cleanup from previous runs - # Clean it up! - if prefix.exists(venv): - shutil.rmtree(prefix.path(venv)) + # There's potentially incomplete cleanup from previous runs + # Clean it up! + if self.prefix.exists(venv): + rmtree(self.prefix.path(venv)) - language.install_environment(prefix, version, deps) - # Write our state to indicate we're installed - state = _state(deps) - _write_state(prefix, venv, state) + lang.install_environment( + self.prefix, self.language_version, self.additional_dependencies, + ) + # Write our state to indicate we're installed + _write_state(self.prefix, venv, _state(self.additional_dependencies)) + + def run(self, file_args): + lang = languages[self.language] + return lang.run_hook(self.prefix, self._asdict(), file_args) + + @classmethod + def create(cls, src, prefix, dct): + # TODO: have cfgv do this (?) + extra_keys = set(dct) - set(_KEYS) + if extra_keys: + logger.warning( + 'Unexpected keys present on {} => {}: ' + '{}'.format(src, dct['id'], ', '.join(sorted(extra_keys))), + ) + return cls(src=src, prefix=prefix, **{k: dct[k] for k in _KEYS}) def _hook(*hook_dicts): @@ -129,169 +138,126 @@ def _hook(*hook_dicts): def _hook_from_manifest_dct(dct): - dct = validate(apply_defaults(dct, MANIFEST_HOOK_DICT), MANIFEST_HOOK_DICT) + dct = apply_defaults(dct, MANIFEST_HOOK_DICT) + dct = validate(dct, MANIFEST_HOOK_DICT) dct = _hook(dct) return dct -class Repository(object): - def __init__(self, repo_config, store): - self.repo_config = repo_config - self.store = store - self.__installed = False - - @classmethod - def create(cls, config, store): - if is_local_repo(config): - return LocalRepository(config, store) - elif is_meta_repo(config): - return MetaRepository(config, store) - else: - return cls(config, store) - - @cached_property - def manifest_hooks(self): - repo, rev = self.repo_config['repo'], self.repo_config['rev'] - repo_path = self.store.clone(repo, rev) - manifest_path = os.path.join(repo_path, C.MANIFEST_FILE) - return {hook['id']: hook for hook in load_manifest(manifest_path)} - - @cached_property - def hooks(self): - for hook in self.repo_config['hooks']: - if hook['id'] not in self.manifest_hooks: - logger.error( - '`{}` is not present in repository {}. ' - 'Typo? Perhaps it is introduced in a newer version? ' - 'Often `pre-commit autoupdate` fixes this.'.format( - hook['id'], self.repo_config['repo'], - ), - ) - exit(1) - - return tuple( - (hook['id'], _hook(self.manifest_hooks[hook['id']], hook)) - for hook in self.repo_config['hooks'] - ) - - def _prefix_from_deps(self, language_name, deps): - repo, rev = self.repo_config['repo'], self.repo_config['rev'] - return Prefix(self.store.clone(repo, rev, deps)) - - def _venvs(self): - ret = set() - for _, hook in self.hooks: - language = hook['language'] - version = hook['language_version'] - deps = tuple(hook['additional_dependencies']) - ret.add(( - self._prefix_from_deps(language, deps), - language, version, deps, - )) - return tuple(ret) - - def require_installed(self): - if not self.__installed: - _install_all(self._venvs(), self.repo_config['repo'], self.store) - self.__installed = True - - def run_hook(self, hook, file_args): - """Run a hook. - - :param dict hook: - :param tuple file_args: all the files to run the hook on - """ - self.require_installed() - language_name = hook['language'] - deps = hook['additional_dependencies'] - prefix = self._prefix_from_deps(language_name, deps) - return languages[language_name].run_hook(prefix, hook, file_args) - - -class LocalRepository(Repository): - def _prefix_from_deps(self, language_name, deps): - """local repositories have a prefix per hook""" +def _local_repository_hooks(repo_config, store): + def _local_prefix(language_name, deps): language = languages[language_name] # pcre / pygrep / script / system / docker_image do not have # environments so they work out of the current directory if language.ENVIRONMENT_DIR is None: - return Prefix(git.get_root()) + return Prefix(os.getcwd()) else: - return Prefix(self.store.make_local(deps)) + return Prefix(store.make_local(deps)) + + hook_dcts = [_hook_from_manifest_dct(h) for h in repo_config['hooks']] + return tuple( + Hook.create( + repo_config['repo'], + _local_prefix(hook['language'], hook['additional_dependencies']), + hook, + ) + for hook in hook_dcts + ) - @property - def manifest(self): - raise NotImplementedError - - @cached_property - def hooks(self): - return tuple( - (hook['id'], _hook_from_manifest_dct(hook)) - for hook in self.repo_config['hooks'] + +def _meta_repository_hooks(repo_config, store): + # imported here to prevent circular imports. + from pre_commit.meta_hooks import check_hooks_apply + from pre_commit.meta_hooks import check_useless_excludes + from pre_commit.meta_hooks import identity + + meta_hooks = [ + _hook_from_manifest_dct(mod.HOOK_DICT) + for mod in (check_hooks_apply, check_useless_excludes, identity) + ] + by_id = {hook['id']: hook for hook in meta_hooks} + + for hook in repo_config['hooks']: + if hook['id'] not in by_id: + logger.error( + '`{}` is not a valid meta hook. ' + 'Typo? Perhaps it is introduced in a newer version? ' + 'Often `pip install --upgrade pre-commit` fixes this.' + .format(hook['id']), + ) + exit(1) + + prefix = Prefix(os.getcwd()) + return tuple( + Hook.create( + repo_config['repo'], + prefix, + _hook(by_id[hook['id']], hook), ) + for hook in repo_config['hooks'] + ) -class MetaRepository(LocalRepository): - @cached_property - def manifest_hooks(self): - # The hooks are imported here to prevent circular imports. - from pre_commit.meta_hooks import check_hooks_apply - from pre_commit.meta_hooks import check_useless_excludes - from pre_commit.meta_hooks import identity - - def _make_entry(mod): - """the hook `entry` is passed through `shlex.split()` by the - command runner, so to prevent issues with spaces and backslashes - (on Windows) it must be quoted here. - """ - return '{} -m {}'.format(pipes.quote(sys.executable), mod.__name__) - - meta_hooks = [ - { - 'id': 'check-hooks-apply', - 'name': 'Check hooks apply to the repository', - 'files': C.CONFIG_FILE, - 'language': 'system', - 'entry': _make_entry(check_hooks_apply), - }, - { - 'id': 'check-useless-excludes', - 'name': 'Check for useless excludes', - 'files': C.CONFIG_FILE, - 'language': 'system', - 'entry': _make_entry(check_useless_excludes), - }, - { - 'id': 'identity', - 'name': 'identity', - 'language': 'system', - 'verbose': True, - 'entry': _make_entry(identity), - }, - ] - - return { - hook['id']: _hook_from_manifest_dct(hook) - for hook in meta_hooks - } - - @cached_property - def hooks(self): - for hook in self.repo_config['hooks']: - if hook['id'] not in self.manifest_hooks: - logger.error( - '`{}` is not a valid meta hook. ' - 'Typo? Perhaps it is introduced in a newer version? ' - 'Often `pip install --upgrade pre-commit` fixes this.' - .format(hook['id']), - ) - exit(1) - - return tuple( - (hook['id'], _hook(self.manifest_hooks[hook['id']], hook)) - for hook in self.repo_config['hooks'] +def _cloned_repository_hooks(repo_config, store): + repo, rev = repo_config['repo'], repo_config['rev'] + manifest_path = os.path.join(store.clone(repo, rev), C.MANIFEST_FILE) + by_id = {hook['id']: hook for hook in load_manifest(manifest_path)} + + for hook in repo_config['hooks']: + if hook['id'] not in by_id: + logger.error( + '`{}` is not present in repository {}. ' + 'Typo? Perhaps it is introduced in a newer version? ' + 'Often `pre-commit autoupdate` fixes this.' + .format(hook['id'], repo), + ) + exit(1) + + hook_dcts = [_hook(by_id[h['id']], h) for h in repo_config['hooks']] + return tuple( + Hook.create( + repo_config['repo'], + Prefix(store.clone(repo, rev, hook['additional_dependencies'])), + hook, ) + for hook in hook_dcts + ) + + +def repository_hooks(repo_config, store): + if is_local_repo(repo_config): + return _local_repository_hooks(repo_config, store) + elif is_meta_repo(repo_config): + return _meta_repository_hooks(repo_config, store) + else: + return _cloned_repository_hooks(repo_config, store) + +def install_hook_envs(hooks, store): + def _need_installed(): + seen = set() + ret = [] + for hook in hooks: + if hook.install_key not in seen and not hook.installed(): + ret.append(hook) + seen.add(hook.install_key) + return ret -def repositories(config, store): - return tuple(Repository.create(x, store) for x in config['repos']) + if not _need_installed(): + return + with store.exclusive_lock(): + # Another process may have already completed this work + need_installed = _need_installed() + if not need_installed: # pragma: no cover (race) + return + + for hook in need_installed: + hook.install() + + +def all_hooks(config, store): + return tuple( + hook + for repo in config['repos'] + for hook in repository_hooks(repo, store) + ) diff --git a/setup.py b/setup.py index edcd04ff8..f6ea719cf 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,6 @@ }, install_requires=[ 'aspy.yaml', - 'cached-property', 'cfgv>=1.0.0', 'identify>=1.0.0', # if this makes it into python3.8 move to extras_require diff --git a/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml index fcba780fd..63e1dd4c6 100644 --- a/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml +++ b/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml @@ -2,5 +2,5 @@ name: Ruby Hook entry: ruby_hook language: ruby - language_version: 2.1.5 + language_version: 2.5.1 files: \.rb$ diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 28b6ab375..0345ea7d0 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -18,6 +18,7 @@ from pre_commit.commands.run import run from pre_commit.util import cmd_output from pre_commit.util import make_executable +from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import add_config_to_repo from testing.fixtures import make_consuming_repo from testing.fixtures import modify_config @@ -362,10 +363,13 @@ def test_merge_conflict_resolved(cap_out, store, in_merge_conflict): ('hooks', 'verbose', 'expected'), ( ([], True, 80), - ([{'id': 'a', 'name': 'a' * 51}], False, 81), - ([{'id': 'a', 'name': 'a' * 51}], True, 85), + ([auto_namedtuple(id='a', name='a' * 51)], False, 81), + ([auto_namedtuple(id='a', name='a' * 51)], True, 85), ( - [{'id': 'a', 'name': 'a' * 51}, {'id': 'b', 'name': 'b' * 52}], + [ + auto_namedtuple(id='a', name='a' * 51), + auto_namedtuple(id='b', name='b' * 52), + ], False, 82, ), diff --git a/tests/repository_test.py b/tests/repository_test.py index 606bfe759..2092802b4 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -20,9 +20,11 @@ from pre_commit.languages import python from pre_commit.languages import ruby from pre_commit.languages import rust -from pre_commit.repository import Repository +from pre_commit.prefix import Prefix +from pre_commit.repository import Hook +from pre_commit.repository import install_hook_envs +from pre_commit.repository import repository_hooks from pre_commit.util import cmd_output -from testing.fixtures import config_with_local_hooks from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo from testing.fixtures import modify_manifest @@ -40,6 +42,13 @@ def _norm_out(b): return b.replace(b'\r\n', b'\n') +def _get_hook(config, store, hook_id): + hooks = repository_hooks(config, store) + install_hook_envs(hooks, store) + hook, = [hook for hook in hooks if hook.id == hook_id] + return hook + + def _test_hook_repo( tempdir_factory, store, @@ -52,11 +61,7 @@ def _test_hook_repo( ): path = make_repo(tempdir_factory, repo_path) config = make_config_from_repo(path, **(config_kwargs or {})) - repo = Repository.create(config, store) - hook_dict, = [ - hook for repo_hook_id, hook in repo.hooks if repo_hook_id == hook_id - ] - ret = repo.run_hook(hook_dict, args) + ret = _get_hook(config, store, hook_id).run(args) assert ret[0] == expected_return_code assert _norm_out(ret[1]) == expected @@ -118,16 +123,9 @@ def test_switch_language_versions_doesnt_clobber(tempdir_factory, store): path = make_repo(tempdir_factory, 'python3_hooks_repo') def run_on_version(version, expected_output): - config = make_config_from_repo( - path, hooks=[{'id': 'python3-hook', 'language_version': version}], - ) - repo = Repository.create(config, store) - hook_dict, = [ - hook - for repo_hook_id, hook in repo.hooks - if repo_hook_id == 'python3-hook' - ] - ret = repo.run_hook(hook_dict, []) + config = make_config_from_repo(path) + config['hooks'][0]['language_version'] = version + ret = _get_hook(config, store, 'python3-hook').run([]) assert ret[0] == 0 assert _norm_out(ret[1]) == expected_output @@ -212,7 +210,7 @@ def test_run_versioned_ruby_hook(tempdir_factory, store): tempdir_factory, store, 'ruby_versioned_hooks_repo', 'ruby_hook', [os.devnull], - b'2.1.5\nHello world from a ruby hook\n', + b'2.5.1\nHello world from a ruby hook\n', ) @@ -234,7 +232,7 @@ def test_run_ruby_hook_with_disable_shared_gems( tempdir_factory, store, 'ruby_versioned_hooks_repo', 'ruby_hook', [os.devnull], - b'2.1.5\nHello world from a ruby hook\n', + b'2.5.1\nHello world from a ruby hook\n', ) @@ -275,10 +273,8 @@ def test_additional_rust_cli_dependencies_installed( config = make_config_from_repo(path) # A small rust package with no dependencies. config['hooks'][0]['additional_dependencies'] = [dep] - repo = Repository.create(config, store) - repo.require_installed() - (prefix, _, _, _), = repo._venvs() - binaries = os.listdir(prefix.path( + hook = _get_hook(config, store, 'rust-hook') + binaries = os.listdir(hook.prefix.path( helpers.environment_dir(rust.ENVIRONMENT_DIR, 'default'), 'bin', )) # normalize for windows @@ -294,10 +290,8 @@ def test_additional_rust_lib_dependencies_installed( # A small rust package with no dependencies. deps = ['shellharden:3.1.0'] config['hooks'][0]['additional_dependencies'] = deps - repo = Repository.create(config, store) - repo.require_installed() - (prefix, _, _, _), = repo._venvs() - binaries = os.listdir(prefix.path( + hook = _get_hook(config, store, 'rust-hook') + binaries = os.listdir(hook.prefix.path( helpers.environment_dir(rust.ENVIRONMENT_DIR, 'default'), 'bin', )) # normalize for windows @@ -362,9 +356,7 @@ def _make_grep_repo(language, entry, store, args=()): ], ), )) - repo = Repository.create(config, store) - (_, hook), = repo.hooks - return repo, hook + return _get_hook(config, store, 'grep-hook') @pytest.fixture @@ -381,21 +373,21 @@ class TestPygrep(object): language = 'pygrep' def test_grep_hook_matching(self, greppable_files, store): - repo, hook = _make_grep_repo(self.language, 'ello', store) - ret, out, _ = repo.run_hook(hook, ('f1', 'f2', 'f3')) + hook = _make_grep_repo(self.language, 'ello', store) + ret, out, _ = hook.run(('f1', 'f2', 'f3')) assert ret == 1 assert _norm_out(out) == b"f1:1:hello'hi\n" def test_grep_hook_case_insensitive(self, greppable_files, store): - repo, hook = _make_grep_repo(self.language, 'ELLO', store, args=['-i']) - ret, out, _ = repo.run_hook(hook, ('f1', 'f2', 'f3')) + hook = _make_grep_repo(self.language, 'ELLO', store, args=['-i']) + ret, out, _ = hook.run(('f1', 'f2', 'f3')) assert ret == 1 assert _norm_out(out) == b"f1:1:hello'hi\n" @pytest.mark.parametrize('regex', ('nope', "foo'bar", r'^\[INFO\]')) def test_grep_hook_not_matching(self, regex, greppable_files, store): - repo, hook = _make_grep_repo(self.language, regex, store) - ret, out, _ = repo.run_hook(hook, ('f1', 'f2', 'f3')) + hook = _make_grep_repo(self.language, regex, store) + ret, out, _ = hook.run(('f1', 'f2', 'f3')) assert (ret, out) == (0, b'') @@ -408,23 +400,19 @@ def test_pcre_hook_many_files(self, greppable_files, store): # This is intended to simulate lots of passing files and one failing # file to make sure it still fails. This is not the case when naively # using a system hook with `grep -H -n '...'` - repo, hook = _make_grep_repo('pcre', 'ello', store) - ret, out, _ = repo.run_hook(hook, (os.devnull,) * 15000 + ('f1',)) + hook = _make_grep_repo('pcre', 'ello', store) + ret, out, _ = hook.run((os.devnull,) * 15000 + ('f1',)) assert ret == 1 assert _norm_out(out) == b"f1:1:hello'hi\n" def test_missing_pcre_support(self, greppable_files, store): - orig_find_executable = parse_shebang.find_executable - def no_grep(exe, **kwargs): - if exe == pcre.GREP: - return None - else: - return orig_find_executable(exe, **kwargs) + assert exe == pcre.GREP + return None with mock.patch.object(parse_shebang, 'find_executable', no_grep): - repo, hook = _make_grep_repo('pcre', 'ello', store) - ret, out, _ = repo.run_hook(hook, ('f1', 'f2', 'f3')) + hook = _make_grep_repo('pcre', 'ello', store) + ret, out, _ = hook.run(('f1', 'f2', 'f3')) assert ret == 1 expected = 'Executable `{}` not found'.format(pcre.GREP).encode() assert out == expected @@ -454,44 +442,23 @@ def test_lots_of_files(tempdir_factory, store): ) -def test_venvs(tempdir_factory, store): - path = make_repo(tempdir_factory, 'python_hooks_repo') - config = make_config_from_repo(path) - repo = Repository.create(config, store) - venv, = repo._venvs() - assert venv == (mock.ANY, 'python', python.get_default_version(), ()) - - -def test_additional_dependencies(tempdir_factory, store): - path = make_repo(tempdir_factory, 'python_hooks_repo') - config = make_config_from_repo(path) - config['hooks'][0]['additional_dependencies'] = ['pep8'] - repo = Repository.create(config, store) - env, = repo._venvs() - assert env == (mock.ANY, 'python', python.get_default_version(), ('pep8',)) - - def test_additional_dependencies_roll_forward(tempdir_factory, store): path = make_repo(tempdir_factory, 'python_hooks_repo') config1 = make_config_from_repo(path) - repo1 = Repository.create(config1, store) - repo1.require_installed() - (prefix1, _, version1, _), = repo1._venvs() - with python.in_env(prefix1, version1): + hook1 = _get_hook(config1, store, 'foo') + with python.in_env(hook1.prefix, hook1.language_version): assert 'mccabe' not in cmd_output('pip', 'freeze', '-l')[1] # Make another repo with additional dependencies config2 = make_config_from_repo(path) config2['hooks'][0]['additional_dependencies'] = ['mccabe'] - repo2 = Repository.create(config2, store) - repo2.require_installed() - (prefix2, _, version2, _), = repo2._venvs() - with python.in_env(prefix2, version2): + hook2 = _get_hook(config2, store, 'foo') + with python.in_env(hook2.prefix, hook2.language_version): assert 'mccabe' in cmd_output('pip', 'freeze', '-l')[1] # should not have affected original - with python.in_env(prefix1, version1): + with python.in_env(hook1.prefix, hook1.language_version): assert 'mccabe' not in cmd_output('pip', 'freeze', '-l')[1] @@ -499,13 +466,10 @@ def test_additional_dependencies_roll_forward(tempdir_factory, store): def test_additional_ruby_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'ruby_hooks_repo') config = make_config_from_repo(path) - config['hooks'][0]['additional_dependencies'] = ['thread_safe', 'tins'] - repo = Repository.create(config, store) - repo.require_installed() - (prefix, _, version, _), = repo._venvs() - with ruby.in_env(prefix, version): + config['hooks'][0]['additional_dependencies'] = ['tins'] + hook = _get_hook(config, store, 'ruby_hook') + with ruby.in_env(hook.prefix, hook.language_version): output = cmd_output('gem', 'list', '--local')[1] - assert 'thread_safe' in output assert 'tins' in output @@ -515,10 +479,8 @@ def test_additional_node_dependencies_installed(tempdir_factory, store): config = make_config_from_repo(path) # Careful to choose a small package that's not depped by npm config['hooks'][0]['additional_dependencies'] = ['lodash'] - repo = Repository.create(config, store) - repo.require_installed() - (prefix, _, version, _), = repo._venvs() - with node.in_env(prefix, version): + hook = _get_hook(config, store, 'foo') + with node.in_env(hook.prefix, hook.language_version): output = cmd_output('npm', 'ls', '-g')[1] assert 'lodash' in output @@ -531,10 +493,8 @@ def test_additional_golang_dependencies_installed( # A small go package deps = ['github.com/golang/example/hello'] config['hooks'][0]['additional_dependencies'] = deps - repo = Repository.create(config, store) - repo.require_installed() - (prefix, _, _, _), = repo._venvs() - binaries = os.listdir(prefix.path( + hook = _get_hook(config, store, 'golang-hook') + binaries = os.listdir(hook.prefix.path( helpers.environment_dir(golang.ENVIRONMENT_DIR, 'default'), 'bin', )) # normalize for windows @@ -553,9 +513,7 @@ def test_local_golang_additional_dependencies(store): 'additional_dependencies': ['github.com/golang/example/hello'], }], } - repo = Repository.create(config, store) - (_, hook), = repo.hooks - ret = repo.run_hook(hook, ('filename',)) + ret = _get_hook(config, store, 'hello').run(()) assert ret[0] == 0 assert _norm_out(ret[1]) == b"Hello, Go examples!\n" @@ -571,9 +529,7 @@ def test_local_rust_additional_dependencies(store): 'additional_dependencies': ['cli:hello-cli:0.2.2'], }], } - repo = Repository.create(config, store) - (_, hook), = repo.hooks - ret = repo.run_hook(hook, ()) + ret = _get_hook(config, store, 'hello').run(()) assert ret[0] == 0 assert _norm_out(ret[1]) == b"Hello World!\n" @@ -589,9 +545,8 @@ def test_fail_hooks(store): 'files': r'changelog/.*(? too-much: foo, hello' + assert fake_log_handler.handle.call_args[0][0].msg == expected + + def test_reinstall(tempdir_factory, store, log_info_mock): path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) - repo = Repository.create(config, store) - repo.require_installed() + _get_hook(config, store, 'foo') # We print some logging during clone (1) + install (3) assert log_info_mock.call_count == 4 log_info_mock.reset_mock() - # Reinstall with same repo should not trigger another install - repo.require_installed() - assert log_info_mock.call_count == 0 # Reinstall on another run should not trigger another install - repo = Repository.create(config, store) - repo.require_installed() + _get_hook(config, store, 'foo') assert log_info_mock.call_count == 0 @@ -622,8 +589,7 @@ def test_control_c_control_c_on_install(tempdir_factory, store): """Regression test for #186.""" path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) - repo = Repository.create(config, store) - hook = repo.hooks[0][1] + hooks = repository_hooks(config, store) class MyKeyboardInterrupt(KeyboardInterrupt): pass @@ -638,16 +604,18 @@ class MyKeyboardInterrupt(KeyboardInterrupt): with mock.patch.object( shutil, 'rmtree', side_effect=MyKeyboardInterrupt, ): - repo.run_hook(hook, []) + install_hook_envs(hooks, store) # Should have made an environment, however this environment is broken! - (prefix, _, version, _), = repo._venvs() - envdir = 'py_env-{}'.format(version) - assert prefix.exists(envdir) + hook, = hooks + assert hook.prefix.exists( + helpers.environment_dir(python.ENVIRONMENT_DIR, hook.language_version), + ) # However, it should be perfectly runnable (reinstall after botched # install) - retv, stdout, stderr = repo.run_hook(hook, []) + install_hook_envs(hooks, store) + retv, stdout, stderr = hook.run(()) assert retv == 0 @@ -656,21 +624,20 @@ def test_invalidated_virtualenv(tempdir_factory, store): # This should not cause every hook in that virtualenv to fail. path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) - repo = Repository.create(config, store) + hook = _get_hook(config, store, 'foo') # Simulate breaking of the virtualenv - repo.require_installed() - (prefix, _, version, _), = repo._venvs() - libdir = prefix.path('py_env-{}'.format(version), 'lib', version) + libdir = hook.prefix.path( + helpers.environment_dir(python.ENVIRONMENT_DIR, hook.language_version), + 'lib', hook.language_version, + ) paths = [ os.path.join(libdir, p) for p in ('site.py', 'site.pyc', '__pycache__') ] cmd_output('rm', '-rf', *paths) # pre-commit should rebuild the virtualenv and it should be runnable - repo = Repository.create(config, store) - hook = repo.hooks[0][1] - retv, stdout, stderr = repo.run_hook(hook, []) + retv, stdout, stderr = _get_hook(config, store, 'foo').run(()) assert retv == 0 @@ -683,57 +650,41 @@ def test_really_long_file_paths(tempdir_factory, store): config = make_config_from_repo(path) with cwd(really_long_path): - repo = Repository.create(config, store) - repo.require_installed() + _get_hook(config, store, 'foo') def test_config_overrides_repo_specifics(tempdir_factory, store): path = make_repo(tempdir_factory, 'script_hooks_repo') config = make_config_from_repo(path) - repo = Repository.create(config, store) - assert repo.hooks[0][1]['files'] == '' + hook = _get_hook(config, store, 'bash_hook') + assert hook.files == '' # Set the file regex to something else config['hooks'][0]['files'] = '\\.sh$' - repo = Repository.create(config, store) - assert repo.hooks[0][1]['files'] == '\\.sh$' + hook = _get_hook(config, store, 'bash_hook') + assert hook.files == '\\.sh$' def _create_repo_with_tags(tempdir_factory, src, tag): path = make_repo(tempdir_factory, src) - with cwd(path): - cmd_output('git', 'tag', tag) + cmd_output('git', 'tag', tag, cwd=path) return path def test_tags_on_repositories(in_tmpdir, tempdir_factory, store): tag = 'v1.1' - git_dir_1 = _create_repo_with_tags(tempdir_factory, 'prints_cwd_repo', tag) - git_dir_2 = _create_repo_with_tags( - tempdir_factory, 'script_hooks_repo', tag, - ) - - repo_1 = Repository.create( - make_config_from_repo(git_dir_1, rev=tag), store, - ) - ret = repo_1.run_hook(repo_1.hooks[0][1], ['-L']) - assert ret[0] == 0 - assert ret[1].strip() == _norm_pwd(in_tmpdir) - - repo_2 = Repository.create( - make_config_from_repo(git_dir_2, rev=tag), store, - ) - ret = repo_2.run_hook(repo_2.hooks[0][1], ['bar']) - assert ret[0] == 0 - assert ret[1] == b'bar\nHello World\n' + git1 = _create_repo_with_tags(tempdir_factory, 'prints_cwd_repo', tag) + git2 = _create_repo_with_tags(tempdir_factory, 'script_hooks_repo', tag) + config1 = make_config_from_repo(git1, rev=tag) + ret1 = _get_hook(config1, store, 'prints_cwd').run(('-L',)) + assert ret1[0] == 0 + assert ret1[1].strip() == _norm_pwd(in_tmpdir) -def test_local_repository(): - config = config_with_local_hooks() - local_repo = Repository.create(config, 'dummy') - with pytest.raises(NotImplementedError): - local_repo.manifest - assert len(local_repo.hooks) == 1 + config2 = make_config_from_repo(git2, rev=tag) + ret2 = _get_hook(config2, store, 'bash_hook').run(('bar',)) + assert ret2[0] == 0 + assert ret2[1] == b'bar\nHello World\n' def test_local_python_repo(store): @@ -744,11 +695,10 @@ def test_local_python_repo(store): dict(hook, additional_dependencies=[repo_path]) for hook in manifest ] config = {'repo': 'local', 'hooks': hooks} - repo = Repository.create(config, store) - (_, hook), = repo.hooks + hook = _get_hook(config, store, 'foo') # language_version should have been adjusted to the interpreter version - assert hook['language_version'] != 'default' - ret = repo.run_hook(hook, ('filename',)) + assert hook.language_version != 'default' + ret = hook.run(('filename',)) assert ret[0] == 0 assert _norm_out(ret[1]) == b"['filename']\nHello World\n" @@ -757,9 +707,8 @@ def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): path = make_repo(tempdir_factory, 'script_hooks_repo') config = make_config_from_repo(path) config['hooks'][0]['id'] = 'i-dont-exist' - repo = Repository.create(config, store) with pytest.raises(SystemExit): - repo.require_installed() + _get_hook(config, store, 'i-dont-exist') assert fake_log_handler.handle.call_args[0][0].msg == ( '`i-dont-exist` is not present in repository file://{}. ' 'Typo? Perhaps it is introduced in a newer version? ' @@ -769,9 +718,8 @@ def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): def test_meta_hook_not_present(store, fake_log_handler): config = {'repo': 'meta', 'hooks': [{'id': 'i-dont-exist'}]} - repo = Repository.create(config, store) with pytest.raises(SystemExit): - repo.require_installed() + _get_hook(config, store, 'i-dont-exist') assert fake_log_handler.handle.call_args[0][0].msg == ( '`i-dont-exist` is not a valid meta hook. ' 'Typo? Perhaps it is introduced in a newer version? ' @@ -784,9 +732,8 @@ def test_too_new_version(tempdir_factory, store, fake_log_handler): with modify_manifest(path) as manifest: manifest[0]['minimum_pre_commit_version'] = '999.0.0' config = make_config_from_repo(path) - repo = Repository.create(config, store) with pytest.raises(SystemExit): - repo.require_installed() + _get_hook(config, store, 'bash_hook') msg = fake_log_handler.handle.call_args[0][0].msg assert re.match( r'^The hook `bash_hook` requires pre-commit version 999\.0\.0 but ' @@ -803,33 +750,35 @@ def test_versions_ok(tempdir_factory, store, version): manifest[0]['minimum_pre_commit_version'] = version config = make_config_from_repo(path) # Should succeed - Repository.create(config, store).require_installed() + _get_hook(config, store, 'bash_hook') def test_manifest_hooks(tempdir_factory, store): path = make_repo(tempdir_factory, 'script_hooks_repo') config = make_config_from_repo(path) - repo = Repository.create(config, store) - - assert repo.manifest_hooks['bash_hook'] == { - 'always_run': False, - 'additional_dependencies': [], - 'args': [], - 'description': '', - 'entry': 'bin/hook.sh', - 'exclude': '^$', - 'files': '', - 'id': 'bash_hook', - 'alias': '', - 'language': 'script', - 'language_version': 'default', - 'log_file': '', - 'minimum_pre_commit_version': '0', - 'name': 'Bash hook', - 'pass_filenames': True, - 'require_serial': False, - 'stages': [], - 'types': ['file'], - 'exclude_types': [], - 'verbose': False, - } + hook = _get_hook(config, store, 'bash_hook') + + assert hook == Hook( + src='file://{}'.format(path), + prefix=Prefix(mock.ANY), + additional_dependencies=[], + alias='', + always_run=False, + args=[], + description='', + entry='bin/hook.sh', + exclude='^$', + exclude_types=[], + files='', + id='bash_hook', + language='script', + language_version='default', + log_file='', + minimum_pre_commit_version='0', + name='Bash hook', + pass_filenames=True, + require_serial=False, + stages=[], + types=['file'], + verbose=False, + ) From e4cf5f321b9d084e61b6165b3951264b22e899e2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 31 Dec 2018 11:15:22 -0800 Subject: [PATCH 315/544] just use normal dicts in tests --- pre_commit/clientlib.py | 3 +- testing/fixtures.py | 37 +++---- tests/clientlib_test.py | 16 +-- tests/commands/autoupdate_test.py | 5 +- tests/commands/run_test.py | 159 +++++++++++++----------------- tests/conftest.py | 24 ++--- tests/repository_test.py | 27 +++-- 7 files changed, 114 insertions(+), 157 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 44599ea6b..07423c34d 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals import argparse -import collections import functools import cfgv @@ -170,7 +169,7 @@ def ordered_load_normalize_legacy_config(contents): data = ordered_load(contents) if isinstance(data, list): # TODO: Once happy, issue a deprecation warning and instructions - return collections.OrderedDict([('repos', data)]) + return {'repos': data} else: return data diff --git a/testing/fixtures.py b/testing/fixtures.py index 91c095a8c..74fe517bb 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -5,7 +5,6 @@ import io import os.path import shutil -from collections import OrderedDict from aspy.yaml import ordered_dump from aspy.yaml import ordered_load @@ -83,30 +82,24 @@ def modify_config(path='.', commit=True): def config_with_local_hooks(): - return OrderedDict(( - ('repo', 'local'), - ( - 'hooks', [OrderedDict(( - ('id', 'do_not_commit'), - ('name', 'Block if "DO NOT COMMIT" is found'), - ('entry', 'DO NOT COMMIT'), - ('language', 'pygrep'), - ('files', '^(.*)$'), - ))], - ), - )) + return { + 'repo': 'local', + 'hooks': [{ + 'id': 'do_not_commit', + 'name': 'Block if "DO NOT COMMIT" is found', + 'entry': 'DO NOT COMMIT', + 'language': 'pygrep', + }], + } def make_config_from_repo(repo_path, rev=None, hooks=None, check=True): manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) - config = OrderedDict(( - ('repo', 'file://{}'.format(repo_path)), - ('rev', rev or git.head_rev(repo_path)), - ( - 'hooks', - hooks or [OrderedDict((('id', hook['id']),)) for hook in manifest], - ), - )) + config = { + 'repo': 'file://{}'.format(repo_path), + 'rev': rev or git.head_rev(repo_path), + 'hooks': hooks or [{'id': hook['id']} for hook in manifest], + } if check: wrapped = validate({'repos': [config]}, CONFIG_SCHEMA) @@ -126,7 +119,7 @@ def read_config(directory, config_file=C.CONFIG_FILE): def write_config(directory, config, config_file=C.CONFIG_FILE): if type(config) is not list and 'repos' not in config: - assert type(config) is OrderedDict + assert isinstance(config, dict), config config = {'repos': [config]} with io.open(os.path.join(directory, config_file), 'w') as outfile: outfile.write(ordered_dump(config, **C.YAML_DUMP_KWARGS)) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index fcd34dc01..c9908a25c 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -11,6 +11,7 @@ from pre_commit.clientlib import MigrateShaToRev from pre_commit.clientlib import validate_config_main from pre_commit.clientlib import validate_manifest_main +from testing.fixtures import config_with_local_hooks from testing.util import get_resource_path @@ -92,18 +93,9 @@ def test_config_valid(config_obj, expected): assert ret is expected -def test_config_with_local_hooks_definition_fails(): - config_obj = {'repos': [{ - 'repo': 'local', - 'rev': 'foo', - 'hooks': [{ - 'id': 'do_not_commit', - 'name': 'Block if "DO NOT COMMIT" is found', - 'entry': 'DO NOT COMMIT', - 'language': 'pcre', - 'files': '^(.*)$', - }], - }]} +def test_local_hooks_with_rev_fails(): + config_obj = {'repos': [config_with_local_hooks()]} + config_obj['repos'][0]['rev'] = 'foo' with pytest.raises(cfgv.ValidationError): cfgv.validate(config_obj, CONFIG_SCHEMA) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index e4d3cc881..089261728 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -3,7 +3,6 @@ import os.path import pipes import shutil -from collections import OrderedDict import pytest @@ -290,7 +289,7 @@ def test_hook_disppearing_repo_raises(hook_disappearing_repo, store): config = make_config_from_repo( hook_disappearing_repo.path, rev=hook_disappearing_repo.original_rev, - hooks=[OrderedDict((('id', 'foo'),))], + hooks=[{'id': 'foo'}], ) with pytest.raises(RepositoryCannotBeUpdatedError): _update_repo(config, store, tags_only=False) @@ -302,7 +301,7 @@ def test_autoupdate_hook_disappearing_repo( config = make_config_from_repo( hook_disappearing_repo.path, rev=hook_disappearing_repo.original_rev, - hooks=[OrderedDict((('id', 'foo'),))], + hooks=[{'id': 'foo'}], check=False, ) write_config('.', config) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 0345ea7d0..84ab1b2c8 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -5,7 +5,6 @@ import os.path import subprocess import sys -from collections import OrderedDict import pytest @@ -521,21 +520,19 @@ def test_lots_of_files(store, tempdir_factory): def test_stages(cap_out, store, repo_with_passing_hook): - config = OrderedDict(( - ('repo', 'local'), - ( - 'hooks', tuple( - { - 'id': 'do-not-commit-{}'.format(i), - 'name': 'hook {}'.format(i), - 'entry': 'DO NOT COMMIT', - 'language': 'pygrep', - 'stages': [stage], - } - for i, stage in enumerate(('commit', 'push', 'manual'), 1) - ), - ), - )) + config = { + 'repo': 'local', + 'hooks': [ + { + 'id': 'do-not-commit-{}'.format(i), + 'name': 'hook {}'.format(i), + 'entry': 'DO NOT COMMIT', + 'language': 'pygrep', + 'stages': [stage], + } + for i, stage in enumerate(('commit', 'push', 'manual'), 1) + ], + } add_config_to_repo(repo_with_passing_hook, config) stage_a_file() @@ -570,26 +567,24 @@ def test_commit_msg_hook(cap_out, store, commit_msg_repo): def test_local_hook_passes(cap_out, store, repo_with_passing_hook): - config = OrderedDict(( - ('repo', 'local'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'flake8'), - ('name', 'flake8'), - ('entry', "'{}' -m flake8".format(sys.executable)), - ('language', 'system'), - ('files', r'\.py$'), - )), OrderedDict(( - ('id', 'do_not_commit'), - ('name', 'Block if "DO NOT COMMIT" is found'), - ('entry', 'DO NOT COMMIT'), - ('language', 'pygrep'), - ('files', '^(.*)$'), - )), - ), - ), - )) + config = { + 'repo': 'local', + 'hooks': [ + { + 'id': 'flake8', + 'name': 'flake8', + 'entry': "'{}' -m flake8".format(sys.executable), + 'language': 'system', + 'files': r'\.py$', + }, + { + 'id': 'do_not_commit', + 'name': 'Block if "DO NOT COMMIT" is found', + 'entry': 'DO NOT COMMIT', + 'language': 'pygrep', + }, + ], + } add_config_to_repo(repo_with_passing_hook, config) with io.open('dummy.py', 'w') as staged_file: @@ -608,18 +603,15 @@ def test_local_hook_passes(cap_out, store, repo_with_passing_hook): def test_local_hook_fails(cap_out, store, repo_with_passing_hook): - config = OrderedDict(( - ('repo', 'local'), - ( - 'hooks', [OrderedDict(( - ('id', 'no-todo'), - ('name', 'No TODO'), - ('entry', 'sh -c "! grep -iI todo $@" --'), - ('language', 'system'), - ('files', ''), - ))], - ), - )) + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'no-todo', + 'name': 'No TODO', + 'entry': 'sh -c "! grep -iI todo $@" --', + 'language': 'system', + }], + } add_config_to_repo(repo_with_passing_hook, config) with io.open('dummy.py', 'w') as staged_file: @@ -638,17 +630,15 @@ def test_local_hook_fails(cap_out, store, repo_with_passing_hook): def test_pcre_deprecation_warning(cap_out, store, repo_with_passing_hook): - config = OrderedDict(( - ('repo', 'local'), - ( - 'hooks', [OrderedDict(( - ('id', 'pcre-hook'), - ('name', 'pcre-hook'), - ('language', 'pcre'), - ('entry', '.'), - ))], - ), - )) + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'pcre-hook', + 'name': 'pcre-hook', + 'language': 'pcre', + 'entry': '.', + }], + } add_config_to_repo(repo_with_passing_hook, config) _test_run( @@ -666,16 +656,10 @@ def test_pcre_deprecation_warning(cap_out, store, repo_with_passing_hook): def test_meta_hook_passes(cap_out, store, repo_with_passing_hook): - config = OrderedDict(( - ('repo', 'meta'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'check-useless-excludes'), - )), - ), - ), - )) + config = { + 'repo': 'meta', + 'hooks': [{'id': 'check-useless-excludes'}], + } add_config_to_repo(repo_with_passing_hook, config) _test_run( @@ -810,25 +794,24 @@ def test_include_exclude_exclude_removes_files(some_filenames): def test_args_hook_only(cap_out, store, repo_with_passing_hook): - config = OrderedDict(( - ('repo', 'local'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'flake8'), - ('name', 'flake8'), - ('entry', "'{}' -m flake8".format(sys.executable)), - ('language', 'system'), - ('stages', ['commit']), - )), OrderedDict(( - ('id', 'do_not_commit'), - ('name', 'Block if "DO NOT COMMIT" is found'), - ('entry', 'DO NOT COMMIT'), - ('language', 'pygrep'), - )), - ), - ), - )) + config = { + 'repo': 'local', + 'hooks': [ + { + 'id': 'flake8', + 'name': 'flake8', + 'entry': "'{}' -m flake8".format(sys.executable), + 'language': 'system', + 'stages': ['commit'], + }, + { + 'id': 'do_not_commit', + 'name': 'Block if "DO NOT COMMIT" is found', + 'entry': 'DO NOT COMMIT', + 'language': 'pygrep', + }, + ], + } add_config_to_repo(repo_with_passing_hook, config) stage_a_file() ret, printed = _do_run( diff --git a/tests/conftest.py b/tests/conftest.py index f72af094c..7479a7b77 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,6 @@ from __future__ import absolute_import from __future__ import unicode_literals -import collections import functools import io import logging @@ -120,19 +119,16 @@ def in_conflicting_submodule(tempdir_factory): @pytest.fixture def commit_msg_repo(tempdir_factory): path = git_dir(tempdir_factory) - config = collections.OrderedDict(( - ('repo', 'local'), - ( - 'hooks', - [collections.OrderedDict(( - ('id', 'must-have-signoff'), - ('name', 'Must have "Signed off by:"'), - ('entry', 'grep -q "Signed off by:"'), - ('language', 'system'), - ('stages', ['commit-msg']), - ))], - ), - )) + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'must-have-signoff', + 'name': 'Must have "Signed off by:"', + 'entry': 'grep -q "Signed off by:"', + 'language': 'system', + 'stages': ['commit-msg'], + }], + } write_config(path, config) with cwd(path): cmd_output('git', 'add', '.') diff --git a/tests/repository_test.py b/tests/repository_test.py index 2092802b4..0286423b9 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1,7 +1,6 @@ from __future__ import absolute_import from __future__ import unicode_literals -import collections import os.path import re import shutil @@ -341,21 +340,17 @@ def test_run_hook_with_curly_braced_arguments(tempdir_factory, store): def _make_grep_repo(language, entry, store, args=()): - config = collections.OrderedDict(( - ('repo', 'local'), - ( - 'hooks', [ - collections.OrderedDict(( - ('id', 'grep-hook'), - ('name', 'grep-hook'), - ('language', language), - ('entry', entry), - ('args', args), - ('types', ['text']), - )), - ], - ), - )) + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'grep-hook', + 'name': 'grep-hook', + 'language': language, + 'entry': entry, + 'args': args, + 'types': ['text'], + }], + } return _get_hook(config, store, 'grep-hook') From b59d7197ff5eed28bf96a2034f7e9aacd03e8376 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 31 Dec 2018 13:16:48 -0800 Subject: [PATCH 316/544] Use Hook api in languages --- pre_commit/languages/all.py | 5 ++--- pre_commit/languages/docker.py | 6 +++--- pre_commit/languages/docker_image.py | 2 +- pre_commit/languages/fail.py | 4 ++-- pre_commit/languages/golang.py | 4 ++-- pre_commit/languages/helpers.py | 4 ++-- pre_commit/languages/node.py | 4 ++-- pre_commit/languages/pcre.py | 4 ++-- pre_commit/languages/pygrep.py | 5 ++--- pre_commit/languages/python.py | 4 ++-- pre_commit/languages/ruby.py | 4 ++-- pre_commit/languages/rust.py | 4 ++-- pre_commit/languages/script.py | 4 ++-- pre_commit/languages/swift.py | 4 ++-- pre_commit/languages/system.py | 2 +- pre_commit/repository.py | 2 +- tests/languages/all_test.py | 2 +- tests/languages/helpers_test.py | 15 ++++++++++----- 18 files changed, 41 insertions(+), 38 deletions(-) diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index a019ddffc..fecce4713 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -39,13 +39,12 @@ # 'default'. # """ # -# def run_hook(prefix, hook, file_args): +# def run_hook(hook, file_args): # """Runs a hook and returns the returncode and output of running that # hook. # # Args: -# prefix - `Prefix` bound to the repository. -# hook - Hook dictionary +# hook - `Hook` # file_args - The files to be run # # Returns: diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index bfdd35854..35b2eda04 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -85,15 +85,15 @@ def docker_cmd(): ) -def run_hook(prefix, hook, file_args): # pragma: windows no cover +def run_hook(hook, file_args): # pragma: windows no cover assert_docker_available() # Rebuild the docker image in case it has gone missing, as many people do # automated cleanup of docker images. - build_docker_image(prefix, pull=False) + build_docker_image(hook.prefix, pull=False) hook_cmd = helpers.to_cmd(hook) entry_exe, cmd_rest = hook_cmd[0], hook_cmd[1:] - entry_tag = ('--entrypoint', entry_exe, docker_tag(prefix)) + entry_tag = ('--entrypoint', entry_exe, docker_tag(hook.prefix)) cmd = docker_cmd() + entry_tag + cmd_rest return helpers.run_xargs(hook, cmd, file_args) diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index e7ebad7f0..ab2a85654 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -12,7 +12,7 @@ install_environment = helpers.no_install -def run_hook(prefix, hook, file_args): # pragma: windows no cover +def run_hook(hook, file_args): # pragma: windows no cover assert_docker_available() cmd = docker_cmd() + helpers.to_cmd(hook) return helpers.run_xargs(hook, cmd, file_args) diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index c69fcae0d..f2ce09e10 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -9,7 +9,7 @@ install_environment = helpers.no_install -def run_hook(prefix, hook, file_args): - out = hook['entry'].encode('UTF-8') + b'\n\n' +def run_hook(hook, file_args): + out = hook.entry.encode('UTF-8') + b'\n\n' out += b'\n'.join(f.encode('UTF-8') for f in file_args) + b'\n' return 1, out, b'' diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 09e3476c5..92d5d36ce 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -78,6 +78,6 @@ def install_environment(prefix, version, additional_dependencies): rmtree(pkgdir) -def run_hook(prefix, hook, file_args): - with in_env(prefix): +def run_hook(hook, file_args): + with in_env(hook.prefix): return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 28b9cb879..faff14379 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -26,7 +26,7 @@ def environment_dir(ENVIRONMENT_DIR, language_version): def to_cmd(hook): - return tuple(shlex.split(hook['entry'])) + tuple(hook['args']) + return tuple(shlex.split(hook.entry)) + tuple(hook.args) def assert_version_default(binary, version): @@ -57,7 +57,7 @@ def no_install(prefix, version, additional_dependencies): def target_concurrency(hook): - if hook['require_serial'] or 'PRE_COMMIT_NO_CONCURRENCY' in os.environ: + if hook.require_serial or 'PRE_COMMIT_NO_CONCURRENCY' in os.environ: return 1 else: # Travis appears to have a bunch of CPUs, but we can't use them all. diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 2e9e60e4f..07f785eaf 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -68,6 +68,6 @@ def install_environment(prefix, version, additional_dependencies): ) -def run_hook(prefix, hook, file_args): - with in_env(prefix, hook['language_version']): +def run_hook(hook, file_args): + with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py index fb078ab78..143adb231 100644 --- a/pre_commit/languages/pcre.py +++ b/pre_commit/languages/pcre.py @@ -13,9 +13,9 @@ install_environment = helpers.no_install -def run_hook(prefix, hook, file_args): +def run_hook(hook, file_args): # For PCRE the entry is the regular expression to match - cmd = (GREP, '-H', '-n', '-P') + tuple(hook['args']) + (hook['entry'],) + cmd = (GREP, '-H', '-n', '-P') + tuple(hook.args) + (hook.entry,) # Grep usually returns 0 for matches, and nonzero for non-matches so we # negate it here. diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 7eead9e1b..e0188a974 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -44,9 +44,8 @@ def _process_filename_at_once(pattern, filename): return retv -def run_hook(prefix, hook, file_args): - exe = (sys.executable, '-m', __name__) - exe += tuple(hook['args']) + (hook['entry'],) +def run_hook(hook, file_args): + exe = (sys.executable, '-m', __name__) + tuple(hook.args) + (hook.entry,) return xargs(exe, file_args) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 4b7580a4e..fab5450a6 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -124,8 +124,8 @@ def healthy(prefix, language_version): ) return retcode == 0 - def run_hook(prefix, hook, file_args): - with in_env(prefix, hook['language_version']): + def run_hook(hook, file_args): + with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) def install_environment(prefix, version, additional_dependencies): diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 484df47c7..04a74155b 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -123,6 +123,6 @@ def install_environment( ) -def run_hook(prefix, hook, file_args): # pragma: windows no cover - with in_env(prefix, hook['language_version']): +def run_hook(hook, file_args): # pragma: windows no cover + with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 8a5a07048..e81fbad26 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -88,6 +88,6 @@ def install_environment(prefix, version, additional_dependencies): ) -def run_hook(prefix, hook, file_args): - with in_env(prefix): +def run_hook(hook, file_args): + with in_env(hook.prefix): return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 809efb854..56d9d27e9 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -9,7 +9,7 @@ install_environment = helpers.no_install -def run_hook(prefix, hook, file_args): +def run_hook(hook, file_args): cmd = helpers.to_cmd(hook) - cmd = (prefix.path(cmd[0]),) + cmd[1:] + cmd = (hook.prefix.path(cmd[0]),) + cmd[1:] return helpers.run_xargs(hook, cmd, file_args) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index c282de5d9..5841f25e5 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -50,6 +50,6 @@ def install_environment( ) -def run_hook(prefix, hook, file_args): # pragma: windows no cover - with in_env(prefix): +def run_hook(hook, file_args): # pragma: windows no cover + with in_env(hook.prefix): return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index e590d4868..5a22670e0 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -9,5 +9,5 @@ install_environment = helpers.no_install -def run_hook(prefix, hook, file_args): +def run_hook(hook, file_args): return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index c9115dfd9..7b980928c 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -100,7 +100,7 @@ def install(self): def run(self, file_args): lang = languages[self.language] - return lang.run_hook(self.prefix, self._asdict(), file_args) + return lang.run_hook(self, file_args) @classmethod def create(cls, src, prefix, dct): diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index 3d5d88c76..967544198 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -39,7 +39,7 @@ def test_ENVIRONMENT_DIR(language): @pytest.mark.parametrize('language', all_languages) def test_run_hook_argpsec(language): - expected_argspec = ArgSpec(args=['prefix', 'hook', 'file_args']) + expected_argspec = ArgSpec(args=['hook', 'file_args']) argspec = getargspec(languages[language].run_hook) assert argspec == expected_argspec diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index f77c3053c..b3360820d 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -11,6 +11,7 @@ from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError +from testing.auto_namedtuple import auto_namedtuple def test_basic_get_default_version(): @@ -33,27 +34,31 @@ def test_failed_setup_command_does_not_unicode_error(): helpers.run_setup_cmd(Prefix('.'), (sys.executable, '-c', script)) +SERIAL_FALSE = auto_namedtuple(require_serial=False) +SERIAL_TRUE = auto_namedtuple(require_serial=True) + + def test_target_concurrency_normal(): with mock.patch.object(multiprocessing, 'cpu_count', return_value=123): with mock.patch.dict(os.environ, {}, clear=True): - assert helpers.target_concurrency({'require_serial': False}) == 123 + assert helpers.target_concurrency(SERIAL_FALSE) == 123 def test_target_concurrency_cpu_count_require_serial_true(): with mock.patch.dict(os.environ, {}, clear=True): - assert helpers.target_concurrency({'require_serial': True}) == 1 + assert helpers.target_concurrency(SERIAL_TRUE) == 1 def test_target_concurrency_testing_env_var(): with mock.patch.dict( os.environ, {'PRE_COMMIT_NO_CONCURRENCY': '1'}, clear=True, ): - assert helpers.target_concurrency({'require_serial': False}) == 1 + assert helpers.target_concurrency(SERIAL_FALSE) == 1 def test_target_concurrency_on_travis(): with mock.patch.dict(os.environ, {'TRAVIS': '1'}, clear=True): - assert helpers.target_concurrency({'require_serial': False}) == 2 + assert helpers.target_concurrency(SERIAL_FALSE) == 2 def test_target_concurrency_cpu_count_not_implemented(): @@ -61,7 +66,7 @@ def test_target_concurrency_cpu_count_not_implemented(): multiprocessing, 'cpu_count', side_effect=NotImplementedError, ): with mock.patch.dict(os.environ, {}, clear=True): - assert helpers.target_concurrency({'require_serial': False}) == 1 + assert helpers.target_concurrency(SERIAL_FALSE) == 1 def test_shuffled_is_deterministic(): From 4f9d0397b564d28cd888d68ce678280efd3562c8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 31 Dec 2018 13:33:28 -0800 Subject: [PATCH 317/544] Add more 'no cover windows' comments --- pre_commit/commands/install_uninstall.py | 2 +- pre_commit/languages/docker.py | 2 +- tests/languages/helpers_test.py | 9 +++++++++ tests/prefix_test.py | 6 ++++++ tests/repository_test.py | 12 ++++++------ tox.ini | 3 +-- 6 files changed, 24 insertions(+), 10 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index e27c5b2c7..a5df93126 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -39,7 +39,7 @@ def _hook_paths(hook_type): def is_our_script(filename): - if not os.path.exists(filename): + if not os.path.exists(filename): # pragma: windows no cover (symlink) return False with io.open(filename) as f: contents = f.read() diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 35b2eda04..e5f3a36b8 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -72,7 +72,7 @@ def install_environment( os.mkdir(directory) -def docker_cmd(): +def docker_cmd(): # pragma: windows no cover return ( 'docker', 'run', '--rm', diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index b3360820d..831e0d598 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -34,6 +34,15 @@ def test_failed_setup_command_does_not_unicode_error(): helpers.run_setup_cmd(Prefix('.'), (sys.executable, '-c', script)) +def test_assert_no_additional_deps(): + with pytest.raises(AssertionError) as excinfo: + helpers.assert_no_additional_deps('lang', ['hmmm']) + msg, = excinfo.value.args + assert msg == ( + 'For now, pre-commit does not support additional_dependencies for lang' + ) + + SERIAL_FALSE = auto_namedtuple(require_serial=False) SERIAL_TRUE = auto_namedtuple(require_serial=True) diff --git a/tests/prefix_test.py b/tests/prefix_test.py index 728b5df42..2806cff1a 100644 --- a/tests/prefix_test.py +++ b/tests/prefix_test.py @@ -38,3 +38,9 @@ def test_exists(tmpdir): assert not Prefix(str(tmpdir)).exists('foo') tmpdir.ensure('foo') assert Prefix(str(tmpdir)).exists('foo') + + +def test_star(tmpdir): + for f in ('a.txt', 'b.txt', 'c.py'): + tmpdir.join(f).ensure() + assert set(Prefix(str(tmpdir)).star('.txt')) == {'a.txt', 'b.txt'} diff --git a/tests/repository_test.py b/tests/repository_test.py index 0286423b9..eecf67b6a 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -141,7 +141,7 @@ def test_versioned_python_hook(tempdir_factory, store): ) -@skipif_cant_run_docker +@skipif_cant_run_docker # pragma: windows no cover def test_run_a_docker_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'docker_hooks_repo', @@ -150,7 +150,7 @@ def test_run_a_docker_hook(tempdir_factory, store): ) -@skipif_cant_run_docker +@skipif_cant_run_docker # pragma: windows no cover def test_run_a_docker_hook_with_entry_args(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'docker_hooks_repo', @@ -159,7 +159,7 @@ def test_run_a_docker_hook_with_entry_args(tempdir_factory, store): ) -@skipif_cant_run_docker +@skipif_cant_run_docker # pragma: windows no cover def test_run_a_failing_docker_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'docker_hooks_repo', @@ -169,7 +169,7 @@ def test_run_a_failing_docker_hook(tempdir_factory, store): ) -@skipif_cant_run_docker +@skipif_cant_run_docker # pragma: windows no cover @pytest.mark.parametrize('hook_id', ('echo-entrypoint', 'echo-cmd')) def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): _test_hook_repo( @@ -242,7 +242,7 @@ def test_system_hook_with_spaces(tempdir_factory, store): ) -@skipif_cant_run_swift +@skipif_cant_run_swift # pragma: windows no cover def test_swift_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'swift_hooks_repo', @@ -386,7 +386,7 @@ def test_grep_hook_not_matching(self, regex, greppable_files, store): assert (ret, out) == (0, b'') -@xfailif_no_pcre_support +@xfailif_no_pcre_support # pragma: windows no cover class TestPCRE(TestPygrep): """organized as a class for xfailing pcre""" language = 'pcre' diff --git a/tox.ini b/tox.ini index 52f3d3ee1..aaeadc28e 100644 --- a/tox.ini +++ b/tox.ini @@ -9,8 +9,7 @@ passenv = GOROOT HOME HOMEPATH PROGRAMDATA TERM commands = coverage erase coverage run -m pytest {posargs:tests} - # TODO: change to 100 - coverage report --fail-under 99 + coverage report --fail-under 100 pre-commit run --all-files [testenv:venv] From 4da461d90aa55d55859cd3e6160fe2db041db305 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 1 Jan 2019 11:57:06 -0800 Subject: [PATCH 318/544] Fix try-repo relpath while in a sub-directory --- pre_commit/main.py | 4 ++++ tests/main_test.py | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/pre_commit/main.py b/pre_commit/main.py index a5a4a817d..99f340700 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -94,12 +94,16 @@ def _adjust_args_and_chdir(args): args.config = os.path.abspath(args.config) if args.command in {'run', 'try-repo'}: args.files = [os.path.abspath(filename) for filename in args.files] + if args.command == 'try-repo' and os.path.exists(args.repo): + args.repo = os.path.abspath(args.repo) os.chdir(git.get_root()) args.config = os.path.relpath(args.config) if args.command in {'run', 'try-repo'}: args.files = [os.path.relpath(filename) for filename in args.files] + if args.command == 'try-repo' and os.path.exists(args.repo): + args.repo = os.path.relpath(args.repo) def main(argv=None): diff --git a/tests/main_test.py b/tests/main_test.py index 83e7d22f4..83758bf4d 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -47,6 +47,17 @@ def test_adjust_args_and_chdir_non_relative_config(in_git_dir): assert args.config == C.CONFIG_FILE +def test_adjust_args_try_repo_repo_relative(in_git_dir): + in_git_dir.join('foo').ensure_dir().chdir() + + args = Args(command='try-repo', repo='../foo', files=[]) + assert os.path.exists(args.repo) + main._adjust_args_and_chdir(args) + assert os.getcwd() == in_git_dir + assert os.path.exists(args.repo) + assert args.repo == 'foo' + + FNS = ( 'autoupdate', 'clean', 'install', 'install_hooks', 'migrate_config', 'run', 'sample_config', 'uninstall', From bdc58cc33f5cecef4c74a6a2f630a052b2222af9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 1 Jan 2019 13:12:02 -0800 Subject: [PATCH 319/544] Teach pre-commit try-repo to clone uncommitted changes --- pre_commit/commands/run.py | 6 +-- pre_commit/commands/try_repo.py | 36 ++++++++++++++++-- pre_commit/git.py | 48 ++++++++++++++++++----- pre_commit/main.py | 10 ++++- pre_commit/store.py | 11 ++---- pre_commit/util.py | 15 -------- testing/fixtures.py | 5 ++- tests/commands/try_repo_test.py | 67 +++++++++++++++++++++++++++------ tests/git_test.py | 6 --- tests/main_test.py | 6 +++ 10 files changed, 148 insertions(+), 62 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 2b90e44e6..f38b25c72 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -199,11 +199,7 @@ def _run_hooks(config, hooks, args, environ): retval |= _run_single_hook(filenames, hook, args, skips, cols) if retval and config['fail_fast']: break - if ( - retval and - args.show_diff_on_failure and - subprocess.call(('git', 'diff', '--quiet', '--no-ext-diff')) != 0 - ): + if retval and args.show_diff_on_failure and git.has_diff(): output.write_line('All changes made by hooks:') subprocess.call(('git', '--no-pager', 'diff', '--no-ext-diff')) return retval diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index e964987c7..c9849ea4c 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import collections +import logging import os.path from aspy.yaml import ordered_dump @@ -12,23 +13,50 @@ from pre_commit.clientlib import load_manifest from pre_commit.commands.run import run from pre_commit.store import Store +from pre_commit.util import cmd_output from pre_commit.util import tmpdir +logger = logging.getLogger(__name__) -def try_repo(args): - ref = args.ref or git.head_rev(args.repo) +def _repo_ref(tmpdir, repo, ref): + # if `ref` is explicitly passed, use it + if ref: + return repo, ref + + ref = git.head_rev(repo) + # if it exists on disk, we'll try and clone it with the local changes + if os.path.exists(repo) and git.has_diff('HEAD', repo=repo): + logger.warning('Creating temporary repo with uncommitted changes...') + + shadow = os.path.join(tmpdir, 'shadow-repo') + cmd_output('git', 'clone', repo, shadow) + cmd_output('git', 'checkout', ref, '-b', '_pc_tmp', cwd=shadow) + idx = git.git_path('index', repo=shadow) + objs = git.git_path('objects', repo=shadow) + env = dict(os.environ, GIT_INDEX_FILE=idx, GIT_OBJECT_DIRECTORY=objs) + cmd_output('git', 'add', '-u', cwd=repo, env=env) + git.commit(repo=shadow) + + return shadow, git.head_rev(shadow) + else: + return repo, ref + + +def try_repo(args): with tmpdir() as tempdir: + repo, ref = _repo_ref(tempdir, args.repo, args.ref) + store = Store(tempdir) if args.hook: hooks = [{'id': args.hook}] else: - repo_path = store.clone(args.repo, ref) + repo_path = store.clone(repo, ref) manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) manifest = sorted(manifest, key=lambda hook: hook['id']) hooks = [{'id': hook['id']} for hook in manifest] - items = (('repo', args.repo), ('rev', ref), ('hooks', hooks)) + items = (('repo', repo), ('rev', ref), ('hooks', hooks)) config = {'repos': [collections.OrderedDict(items)]} config_s = ordered_dump(config, **C.YAML_DUMP_KWARGS) diff --git a/pre_commit/git.py b/pre_commit/git.py index 84db66ea4..ccdd18566 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -4,12 +4,10 @@ import os.path import sys -from pre_commit.error_handler import FatalError -from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output -logger = logging.getLogger('pre_commit') +logger = logging.getLogger(__name__) def zsplit(s): @@ -20,14 +18,23 @@ def zsplit(s): return [] +def no_git_env(): + # Too many bugs dealing with environment variables and GIT: + # https://github.com/pre-commit/pre-commit/issues/300 + # In git 2.6.3 (maybe others), git exports GIT_WORK_TREE while running + # pre-commit hooks + # In git 1.9.1 (maybe others), git exports GIT_DIR and GIT_INDEX_FILE + # while running pre-commit hooks in submodules. + # GIT_DIR: Causes git clone to clone wrong thing + # GIT_INDEX_FILE: Causes 'error invalid object ...' during commit + return { + k: v for k, v in os.environ.items() + if not k.startswith('GIT_') or k in {'GIT_SSH'} + } + + def get_root(): - try: - return cmd_output('git', 'rev-parse', '--show-toplevel')[1].strip() - except CalledProcessError: - raise FatalError( - 'git failed. Is it installed, and are you in a Git repository ' - 'directory?', - ) + return cmd_output('git', 'rev-parse', '--show-toplevel')[1].strip() def get_git_dir(git_root='.'): @@ -106,6 +113,27 @@ def head_rev(remote): return out.split()[0] +def has_diff(*args, **kwargs): + repo = kwargs.pop('repo', '.') + assert not kwargs, kwargs + cmd = ('git', 'diff', '--quiet', '--no-ext-diff') + args + return cmd_output(*cmd, cwd=repo, retcode=None)[0] + + +def commit(repo='.'): + env = no_git_env() + name, email = 'pre-commit', 'asottile+pre-commit@umich.edu' + env['GIT_AUTHOR_NAME'] = env['GIT_COMMITTER_NAME'] = name + env['GIT_AUTHOR_EMAIL'] = env['GIT_COMMITTER_EMAIL'] = email + cmd = ('git', 'commit', '--no-edit', '--no-gpg-sign', '-n', '-minit') + cmd_output(*cmd, cwd=repo, env=env) + + +def git_path(name, repo='.'): + _, out, _ = cmd_output('git', 'rev-parse', '--git-path', name, cwd=repo) + return os.path.join(repo, out.strip()) + + def check_for_cygwin_mismatch(): """See https://github.com/pre-commit/pre-commit/issues/354""" if sys.platform in ('cygwin', 'win32'): # pragma: no cover (windows) diff --git a/pre_commit/main.py b/pre_commit/main.py index 99f340700..71995f15b 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -19,8 +19,10 @@ from pre_commit.commands.sample_config import sample_config from pre_commit.commands.try_repo import try_repo from pre_commit.error_handler import error_handler +from pre_commit.error_handler import FatalError from pre_commit.logging_handler import add_logging_handler from pre_commit.store import Store +from pre_commit.util import CalledProcessError logger = logging.getLogger('pre_commit') @@ -97,7 +99,13 @@ def _adjust_args_and_chdir(args): if args.command == 'try-repo' and os.path.exists(args.repo): args.repo = os.path.abspath(args.repo) - os.chdir(git.get_root()) + try: + os.chdir(git.get_root()) + except CalledProcessError: + raise FatalError( + 'git failed. Is it installed, and are you in a Git repository ' + 'directory?', + ) args.config = os.path.relpath(args.config) if args.command in {'run', 'try-repo'}: diff --git a/pre_commit/store.py b/pre_commit/store.py index f3096fcd0..3200a5675 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -9,9 +9,9 @@ import pre_commit.constants as C from pre_commit import file_lock +from pre_commit import git from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output -from pre_commit.util import no_git_env from pre_commit.util import resource_text @@ -135,7 +135,7 @@ def _get_result(): def clone(self, repo, ref, deps=()): """Clone the given url and checkout the specific ref.""" def clone_strategy(directory): - env = no_git_env() + env = git.no_git_env() cmd = ('git', 'clone', '--no-checkout', repo, directory) cmd_output(*cmd, env=env) @@ -160,10 +160,7 @@ def make_local_strategy(directory): with io.open(os.path.join(directory, resource), 'w') as f: f.write(contents) - env = no_git_env() - name, email = 'pre-commit', 'asottile+pre-commit@umich.edu' - env['GIT_AUTHOR_NAME'] = env['GIT_COMMITTER_NAME'] = name - env['GIT_AUTHOR_EMAIL'] = env['GIT_COMMITTER_EMAIL'] = email + env = git.no_git_env() # initialize the git repository so it looks more like cloned repos def _git_cmd(*args): @@ -172,7 +169,7 @@ def _git_cmd(*args): _git_cmd('init', '.') _git_cmd('config', 'remote.origin.url', '<>') _git_cmd('add', '.') - _git_cmd('commit', '--no-edit', '--no-gpg-sign', '-n', '-minit') + git.commit(repo=directory) return self._new_repo( 'local', C.LOCAL_REPO_VERSION, deps, make_local_strategy, diff --git a/pre_commit/util.py b/pre_commit/util.py index 963461d16..c38af5a28 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -64,21 +64,6 @@ def noop_context(): yield -def no_git_env(): - # Too many bugs dealing with environment variables and GIT: - # https://github.com/pre-commit/pre-commit/issues/300 - # In git 2.6.3 (maybe others), git exports GIT_WORK_TREE while running - # pre-commit hooks - # In git 1.9.1 (maybe others), git exports GIT_DIR and GIT_INDEX_FILE - # while running pre-commit hooks in submodules. - # GIT_DIR: Causes git clone to clone wrong thing - # GIT_INDEX_FILE: Causes 'error invalid object ...' during commit - return { - k: v for k, v in os.environ.items() - if not k.startswith('GIT_') or k in {'GIT_SSH'} - } - - @contextlib.contextmanager def tmpdir(): """Contextmanager to create a temporary directory. It will be cleaned up diff --git a/testing/fixtures.py b/testing/fixtures.py index 74fe517bb..b0606ee44 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -53,7 +53,7 @@ def make_repo(tempdir_factory, repo_source): @contextlib.contextmanager -def modify_manifest(path): +def modify_manifest(path, commit=True): """Modify the manifest yielded by this context to write to .pre-commit-hooks.yaml. """ @@ -63,7 +63,8 @@ def modify_manifest(path): yield manifest with io.open(manifest_path, 'w') as manifest_file: manifest_file.write(ordered_dump(manifest, **C.YAML_DUMP_KWARGS)) - git_commit(msg=modify_manifest.__name__, cwd=path) + if commit: + git_commit(msg=modify_manifest.__name__, cwd=path) @contextlib.contextmanager diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index 66d1642df..5b50f420c 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -4,12 +4,15 @@ import os.path import re +from pre_commit import git from pre_commit.commands.try_repo import try_repo from pre_commit.util import cmd_output from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import git_dir from testing.fixtures import make_repo +from testing.fixtures import modify_manifest from testing.util import cwd +from testing.util import git_commit from testing.util import run_opts @@ -21,22 +24,26 @@ def _get_out(cap_out): out = cap_out.get().replace('\r\n', '\n') out = re.sub(r'\[INFO\].+\n', '', out) start, using_config, config, rest = out.split('=' * 79 + '\n') - assert start == '' assert using_config == 'Using config:\n' - return config, rest + return start, config, rest + + +def _add_test_file(): + open('test-file', 'a').close() + cmd_output('git', 'add', '.') def _run_try_repo(tempdir_factory, **kwargs): repo = make_repo(tempdir_factory, 'modified_file_returns_zero_repo') with cwd(git_dir(tempdir_factory)): - open('test-file', 'a').close() - cmd_output('git', 'add', '.') + _add_test_file() assert not try_repo(try_repo_opts(repo, **kwargs)) def test_try_repo_repo_only(cap_out, tempdir_factory): _run_try_repo(tempdir_factory, verbose=True) - config, rest = _get_out(cap_out) + start, config, rest = _get_out(cap_out) + assert start == '' assert re.match( '^repos:\n' '- repo: .+\n' @@ -48,19 +55,20 @@ def test_try_repo_repo_only(cap_out, tempdir_factory): config, ) assert rest == ( - '[bash_hook] Bash hook................................(no files to check)Skipped\n' # noqa - '[bash_hook2] Bash hook...................................................Passed\n' # noqa + '[bash_hook] Bash hook................................(no files to check)Skipped\n' # noqa: E501 + '[bash_hook2] Bash hook...................................................Passed\n' # noqa: E501 'hookid: bash_hook2\n' '\n' 'test-file\n' '\n' - '[bash_hook3] Bash hook...............................(no files to check)Skipped\n' # noqa + '[bash_hook3] Bash hook...............................(no files to check)Skipped\n' # noqa: E501 ) def test_try_repo_with_specific_hook(cap_out, tempdir_factory): _run_try_repo(tempdir_factory, hook='bash_hook', verbose=True) - config, rest = _get_out(cap_out) + start, config, rest = _get_out(cap_out) + assert start == '' assert re.match( '^repos:\n' '- repo: .+\n' @@ -69,14 +77,49 @@ def test_try_repo_with_specific_hook(cap_out, tempdir_factory): ' - id: bash_hook\n$', config, ) - assert rest == '[bash_hook] Bash hook................................(no files to check)Skipped\n' # noqa + assert rest == '[bash_hook] Bash hook................................(no files to check)Skipped\n' # noqa: E501 def test_try_repo_relative_path(cap_out, tempdir_factory): repo = make_repo(tempdir_factory, 'modified_file_returns_zero_repo') with cwd(git_dir(tempdir_factory)): - open('test-file', 'a').close() - cmd_output('git', 'add', '.') + _add_test_file() relative_repo = os.path.relpath(repo, '.') # previously crashed on cloning a relative path assert not try_repo(try_repo_opts(relative_repo, hook='bash_hook')) + + +def test_try_repo_specific_revision(cap_out, tempdir_factory): + repo = make_repo(tempdir_factory, 'script_hooks_repo') + ref = git.head_rev(repo) + git_commit(cwd=repo) + with cwd(git_dir(tempdir_factory)): + _add_test_file() + assert not try_repo(try_repo_opts(repo, ref=ref)) + + _, config, _ = _get_out(cap_out) + assert ref in config + + +def test_try_repo_uncommitted_changes(cap_out, tempdir_factory): + repo = make_repo(tempdir_factory, 'script_hooks_repo') + # make an uncommitted change + with modify_manifest(repo, commit=False) as manifest: + manifest[0]['name'] = 'modified name!' + + with cwd(git_dir(tempdir_factory)): + open('test-fie', 'a').close() + cmd_output('git', 'add', '.') + assert not try_repo(try_repo_opts(repo)) + + start, config, rest = _get_out(cap_out) + assert start == '[WARNING] Creating temporary repo with uncommitted changes...\n' # noqa: E501 + assert re.match( + '^repos:\n' + '- repo: .+shadow-repo\n' + ' rev: .+\n' + ' hooks:\n' + ' - id: bash_hook\n$', + config, + ) + assert rest == 'modified name!...........................................................Passed\n' # noqa: E501 diff --git a/tests/git_test.py b/tests/git_test.py index cb8a2bf1a..a78b74581 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -7,7 +7,6 @@ import pytest from pre_commit import git -from pre_commit.error_handler import FatalError from pre_commit.util import cmd_output from testing.util import git_commit @@ -23,11 +22,6 @@ def test_get_root_deeper(in_git_dir): assert os.path.normcase(git.get_root()) == expected -def test_get_root_not_git_dir(in_tmpdir): - with pytest.raises(FatalError): - git.get_root() - - def test_get_staged_files_deleted(in_git_dir): in_git_dir.join('test').ensure() cmd_output('git', 'add', 'test') diff --git a/tests/main_test.py b/tests/main_test.py index 83758bf4d..c5db3da16 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -9,6 +9,7 @@ import pre_commit.constants as C from pre_commit import main +from pre_commit.error_handler import FatalError from testing.auto_namedtuple import auto_namedtuple @@ -19,6 +20,11 @@ def __init__(self, **kwargs): self.__dict__.update(kwargs) +def test_adjust_args_and_chdir_not_in_git_dir(in_tmpdir): + with pytest.raises(FatalError): + main._adjust_args_and_chdir(Args()) + + def test_adjust_args_and_chdir_noop(in_git_dir): args = Args(command='run', files=['f1', 'f2']) main._adjust_args_and_chdir(args) From e4f0b4c1b7ec0f1971ecb4532636e70e4d10f08c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 1 Jan 2019 13:33:05 -0800 Subject: [PATCH 320/544] Only configure logging inside the context --- pre_commit/logging_handler.py | 11 +++++++++-- pre_commit/main.py | 6 ++---- tests/conftest.py | 5 +++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/pre_commit/logging_handler.py b/pre_commit/logging_handler.py index c043a8ac2..a1e2c0864 100644 --- a/pre_commit/logging_handler.py +++ b/pre_commit/logging_handler.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import contextlib import logging from pre_commit import color @@ -34,6 +35,12 @@ def emit(self, record): ) -def add_logging_handler(*args, **kwargs): - logger.addHandler(LoggingHandler(*args, **kwargs)) +@contextlib.contextmanager +def logging_handler(*args, **kwargs): + handler = LoggingHandler(*args, **kwargs) + logger.addHandler(handler) logger.setLevel(logging.INFO) + try: + yield + finally: + logger.removeHandler(handler) diff --git a/pre_commit/main.py b/pre_commit/main.py index 71995f15b..6a9c120cd 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -20,7 +20,7 @@ from pre_commit.commands.try_repo import try_repo from pre_commit.error_handler import error_handler from pre_commit.error_handler import FatalError -from pre_commit.logging_handler import add_logging_handler +from pre_commit.logging_handler import logging_handler from pre_commit.store import Store from pre_commit.util import CalledProcessError @@ -248,9 +248,7 @@ def main(argv=None): elif args.command == 'help': parser.parse_args(['--help']) - with error_handler(): - add_logging_handler(args.color) - + with error_handler(), logging_handler(args.color): _adjust_args_and_chdir(args) store = Store() diff --git a/tests/conftest.py b/tests/conftest.py index 7479a7b77..c7d815620 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,7 +11,7 @@ import six from pre_commit import output -from pre_commit.logging_handler import add_logging_handler +from pre_commit.logging_handler import logging_handler from pre_commit.store import Store from pre_commit.util import cmd_output from testing.fixtures import git_dir @@ -155,7 +155,8 @@ class YouForgotToExplicitlyChooseAStoreDirectory(AssertionError): @pytest.fixture(autouse=True, scope='session') def configure_logging(): - add_logging_handler(use_color=False) + with logging_handler(use_color=False): + yield @pytest.fixture From 9e34e6e31689f4f2186df08a12e3a7fb16a54158 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 1 Jan 2019 22:01:10 -0800 Subject: [PATCH 321/544] pre-commit gc --- pre_commit/commands/gc.py | 83 ++++++++++++++++ pre_commit/error_handler.py | 1 - pre_commit/main.py | 12 ++- pre_commit/store.py | 143 +++++++++++++++++---------- testing/fixtures.py | 6 +- tests/clientlib_test.py | 4 +- tests/commands/autoupdate_test.py | 10 +- tests/commands/clean_test.py | 2 +- tests/commands/gc_test.py | 158 ++++++++++++++++++++++++++++++ tests/commands/run_test.py | 7 +- tests/main_test.py | 4 +- tests/store_test.py | 98 +++++++++--------- 12 files changed, 412 insertions(+), 116 deletions(-) create mode 100644 pre_commit/commands/gc.py create mode 100644 tests/commands/gc_test.py diff --git a/pre_commit/commands/gc.py b/pre_commit/commands/gc.py new file mode 100644 index 000000000..9722643d3 --- /dev/null +++ b/pre_commit/commands/gc.py @@ -0,0 +1,83 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import os.path + +import pre_commit.constants as C +from pre_commit import output +from pre_commit.clientlib import InvalidConfigError +from pre_commit.clientlib import InvalidManifestError +from pre_commit.clientlib import is_local_repo +from pre_commit.clientlib import is_meta_repo +from pre_commit.clientlib import load_config +from pre_commit.clientlib import load_manifest + + +def _mark_used_repos(store, all_repos, unused_repos, repo): + if is_meta_repo(repo): + return + elif is_local_repo(repo): + for hook in repo['hooks']: + deps = hook.get('additional_dependencies') + unused_repos.discard(( + store.db_repo_name(repo['repo'], deps), C.LOCAL_REPO_VERSION, + )) + else: + key = (repo['repo'], repo['rev']) + path = all_repos.get(key) + # can't inspect manifest if it isn't cloned + if path is None: + return + + try: + manifest = load_manifest(os.path.join(path, C.MANIFEST_FILE)) + except InvalidManifestError: + return + else: + unused_repos.discard(key) + by_id = {hook['id']: hook for hook in manifest} + + for hook in repo['hooks']: + if hook['id'] not in by_id: + continue + + deps = hook.get( + 'additional_dependencies', + by_id[hook['id']]['additional_dependencies'], + ) + unused_repos.discard(( + store.db_repo_name(repo['repo'], deps), repo['rev'], + )) + + +def _gc_repos(store): + configs = store.select_all_configs() + repos = store.select_all_repos() + + # delete config paths which do not exist + dead_configs = [p for p in configs if not os.path.exists(p)] + live_configs = [p for p in configs if os.path.exists(p)] + + all_repos = {(repo, ref): path for repo, ref, path in repos} + unused_repos = set(all_repos) + for config_path in live_configs: + try: + config = load_config(config_path) + except InvalidConfigError: + dead_configs.append(config_path) + continue + else: + for repo in config['repos']: + _mark_used_repos(store, all_repos, unused_repos, repo) + + store.delete_configs(dead_configs) + for db_repo_name, ref in unused_repos: + store.delete_repo(db_repo_name, ref, all_repos[(db_repo_name, ref)]) + return len(unused_repos) + + +def gc(store): + with store.exclusive_lock(): + repos_removed = _gc_repos(store) + output.write_line('{} repo(s) removed.'.format(repos_removed)) + return 0 diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 720678032..3b0a4c517 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -32,7 +32,6 @@ def _log_and_exit(msg, exc, formatted): )) output.write(error_msg) store = Store() - store.require_created() log_path = os.path.join(store.directory, 'pre-commit.log') output.write_line('Check the log at {}'.format(log_path)) with open(log_path, 'wb') as log: diff --git a/pre_commit/main.py b/pre_commit/main.py index 6a9c120cd..be0fa7f03 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -11,6 +11,7 @@ from pre_commit import git from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.clean import clean +from pre_commit.commands.gc import gc from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import install_hooks from pre_commit.commands.install_uninstall import uninstall @@ -176,6 +177,11 @@ def main(argv=None): ) _add_color_option(clean_parser) _add_config_option(clean_parser) + + gc_parser = subparsers.add_parser('gc', help='Clean unused cached repos.') + _add_color_option(gc_parser) + _add_config_option(gc_parser) + autoupdate_parser = subparsers.add_parser( 'autoupdate', help="Auto-update pre-commit config to the latest repos' versions.", @@ -251,9 +257,11 @@ def main(argv=None): with error_handler(), logging_handler(args.color): _adjust_args_and_chdir(args) - store = Store() git.check_for_cygwin_mismatch() + store = Store() + store.mark_config_used(args.config) + if args.command == 'install': return install( args.config, store, @@ -267,6 +275,8 @@ def main(argv=None): return uninstall(hook_type=args.hook_type) elif args.command == 'clean': return clean(store) + elif args.command == 'gc': + return gc(store) elif args.command == 'autoupdate': if args.tags_only: logger.warning('--tags-only is the default') diff --git a/pre_commit/store.py b/pre_commit/store.py index 3200a5675..8301ecad8 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -13,6 +13,7 @@ from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import resource_text +from pre_commit.util import rmtree logger = logging.getLogger('pre_commit') @@ -33,10 +34,43 @@ def _get_default_directory(): class Store(object): get_default_directory = staticmethod(_get_default_directory) - __created = False def __init__(self, directory=None): self.directory = directory or Store.get_default_directory() + self.db_path = os.path.join(self.directory, 'db.db') + + if not os.path.exists(self.directory): + os.makedirs(self.directory) + with io.open(os.path.join(self.directory, 'README'), 'w') as f: + f.write( + 'This directory is maintained by the pre-commit project.\n' + 'Learn more: https://github.com/pre-commit/pre-commit\n', + ) + + if os.path.exists(self.db_path): + return + with self.exclusive_lock(): + # Another process may have already completed this work + if os.path.exists(self.db_path): # pragma: no cover (race) + return + # To avoid a race where someone ^Cs between db creation and + # execution of the CREATE TABLE statement + fd, tmpfile = tempfile.mkstemp(dir=self.directory) + # We'll be managing this file ourselves + os.close(fd) + with self.connect(db_path=tmpfile) as db: + db.executescript( + 'CREATE TABLE repos (' + ' repo TEXT NOT NULL,' + ' ref TEXT NOT NULL,' + ' path TEXT NOT NULL,' + ' PRIMARY KEY (repo, ref)' + ');', + ) + self._create_config_table_if_not_exists(db) + + # Atomic file move + os.rename(tmpfile, self.db_path) @contextlib.contextmanager def exclusive_lock(self): @@ -46,62 +80,30 @@ def blocked_cb(): # pragma: no cover (tests are single-process) with file_lock.lock(os.path.join(self.directory, '.lock'), blocked_cb): yield - def _write_readme(self): - with io.open(os.path.join(self.directory, 'README'), 'w') as readme: - readme.write( - 'This directory is maintained by the pre-commit project.\n' - 'Learn more: https://github.com/pre-commit/pre-commit\n', - ) - - def _write_sqlite_db(self): - # To avoid a race where someone ^Cs between db creation and execution - # of the CREATE TABLE statement - fd, tmpfile = tempfile.mkstemp(dir=self.directory) - # We'll be managing this file ourselves - os.close(fd) + @contextlib.contextmanager + def connect(self, db_path=None): + db_path = db_path or self.db_path # sqlite doesn't close its fd with its contextmanager >.< # contextlib.closing fixes this. # See: https://stackoverflow.com/a/28032829/812183 - with contextlib.closing(sqlite3.connect(tmpfile)) as db: - db.executescript( - 'CREATE TABLE repos (' - ' repo TEXT NOT NULL,' - ' ref TEXT NOT NULL,' - ' path TEXT NOT NULL,' - ' PRIMARY KEY (repo, ref)' - ');', - ) + with contextlib.closing(sqlite3.connect(db_path)) as db: + # this creates a transaction + with db: + yield db - # Atomic file move - os.rename(tmpfile, self.db_path) - - def _create(self): - if not os.path.exists(self.directory): - os.makedirs(self.directory) - self._write_readme() - - if os.path.exists(self.db_path): - return - with self.exclusive_lock(): - # Another process may have already completed this work - if os.path.exists(self.db_path): # pragma: no cover (race) - return - self._write_sqlite_db() - - def require_created(self): - """Require the pre-commit file store to be created.""" - if not self.__created: - self._create() - self.__created = True + @classmethod + def db_repo_name(cls, repo, deps): + if deps: + return '{}:{}'.format(repo, ','.join(sorted(deps))) + else: + return repo def _new_repo(self, repo, ref, deps, make_strategy): - self.require_created() - if deps: - repo = '{}:{}'.format(repo, ','.join(sorted(deps))) + repo = self.db_repo_name(repo, deps) def _get_result(): # Check if we already exist - with sqlite3.connect(self.db_path) as db: + with self.connect() as db: result = db.execute( 'SELECT path FROM repos WHERE repo = ? AND ref = ?', (repo, ref), @@ -125,7 +127,7 @@ def _get_result(): make_strategy(directory) # Update our db with the created repo - with sqlite3.connect(self.db_path) as db: + with self.connect() as db: db.execute( 'INSERT INTO repos (repo, ref, path) VALUES (?, ?, ?)', [repo, ref, directory], @@ -175,6 +177,43 @@ def _git_cmd(*args): 'local', C.LOCAL_REPO_VERSION, deps, make_local_strategy, ) - @property - def db_path(self): - return os.path.join(self.directory, 'db.db') + def _create_config_table_if_not_exists(self, db): + db.executescript( + 'CREATE TABLE IF NOT EXISTS configs (' + ' path TEXT NOT NULL,' + ' PRIMARY KEY (path)' + ');', + ) + + def mark_config_used(self, path): + path = os.path.realpath(path) + # don't insert config files that do not exist + if not os.path.exists(path): + return + with self.connect() as db: + # TODO: eventually remove this and only create in _create + self._create_config_table_if_not_exists(db) + db.execute('INSERT OR IGNORE INTO configs VALUES (?)', (path,)) + + def select_all_configs(self): + with self.connect() as db: + self._create_config_table_if_not_exists(db) + rows = db.execute('SELECT path FROM configs').fetchall() + return [path for path, in rows] + + def delete_configs(self, configs): + with self.connect() as db: + rows = [(path,) for path in configs] + db.executemany('DELETE FROM configs WHERE path = ?', rows) + + def select_all_repos(self): + with self.connect() as db: + return db.execute('SELECT repo, ref, path from repos').fetchall() + + def delete_repo(self, db_repo_name, ref, path): + with self.connect() as db: + db.execute( + 'DELETE FROM repos WHERE repo = ? and ref = ?', + (db_repo_name, ref), + ) + rmtree(path) diff --git a/testing/fixtures.py b/testing/fixtures.py index b0606ee44..70d0750de 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -82,7 +82,7 @@ def modify_config(path='.', commit=True): git_commit(msg=modify_config.__name__, cwd=path) -def config_with_local_hooks(): +def sample_local_config(): return { 'repo': 'local', 'hooks': [{ @@ -94,6 +94,10 @@ def config_with_local_hooks(): } +def sample_meta_config(): + return {'repo': 'meta', 'hooks': [{'id': 'check-useless-excludes'}]} + + def make_config_from_repo(repo_path, rev=None, hooks=None, check=True): manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) config = { diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index c9908a25c..dbae4aad0 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -11,7 +11,7 @@ from pre_commit.clientlib import MigrateShaToRev from pre_commit.clientlib import validate_config_main from pre_commit.clientlib import validate_manifest_main -from testing.fixtures import config_with_local_hooks +from testing.fixtures import sample_local_config from testing.util import get_resource_path @@ -94,7 +94,7 @@ def test_config_valid(config_obj, expected): def test_local_hooks_with_rev_fails(): - config_obj = {'repos': [config_with_local_hooks()]} + config_obj = {'repos': [sample_local_config()]} config_obj['repos'][0]['rev'] = 'foo' with pytest.raises(cfgv.ValidationError): cfgv.validate(config_obj, CONFIG_SCHEMA) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 089261728..8daf986aa 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -15,9 +15,9 @@ from pre_commit.util import cmd_output from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import add_config_to_repo -from testing.fixtures import config_with_local_hooks from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo +from testing.fixtures import sample_local_config from testing.fixtures import write_config from testing.util import get_resource_path from testing.util import git_commit @@ -125,7 +125,7 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( stale_config = make_config_from_repo( out_of_date_repo.path, rev=out_of_date_repo.original_rev, check=False, ) - local_config = config_with_local_hooks() + local_config = sample_local_config() config = {'repos': [stale_config, local_config]} # Write out the config write_config('.', config) @@ -139,7 +139,7 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( assert ret == 0 assert before != after assert out_of_date_repo.head_rev in after - assert local_config['repo'] in after + assert 'local' in after def test_autoupdate_out_of_date_repo_with_wrong_repo_name( @@ -316,7 +316,7 @@ def test_autoupdate_hook_disappearing_repo( def test_autoupdate_local_hooks(in_git_dir, store): - config = config_with_local_hooks() + config = sample_local_config() add_config_to_repo('.', config) assert autoupdate(C.CONFIG_FILE, store, tags_only=False) == 0 new_config_writen = load_config(C.CONFIG_FILE) @@ -330,7 +330,7 @@ def test_autoupdate_local_hooks_with_out_of_date_repo( stale_config = make_config_from_repo( out_of_date_repo.path, rev=out_of_date_repo.original_rev, check=False, ) - local_config = config_with_local_hooks() + local_config = sample_local_config() config = {'repos': [local_config, stale_config]} write_config('.', config) assert autoupdate(C.CONFIG_FILE, store, tags_only=False) == 0 diff --git a/tests/commands/clean_test.py b/tests/commands/clean_test.py index 3bfa46a3f..dc33ebb07 100644 --- a/tests/commands/clean_test.py +++ b/tests/commands/clean_test.py @@ -21,7 +21,6 @@ def _expanduser(path, *args, **kwargs): def test_clean(store, fake_old_dir): - store.require_created() assert os.path.exists(fake_old_dir) assert os.path.exists(store.directory) clean(store) @@ -30,6 +29,7 @@ def test_clean(store, fake_old_dir): def test_clean_idempotent(store): + clean(store) assert not os.path.exists(store.directory) clean(store) assert not os.path.exists(store.directory) diff --git a/tests/commands/gc_test.py b/tests/commands/gc_test.py new file mode 100644 index 000000000..2f958f67f --- /dev/null +++ b/tests/commands/gc_test.py @@ -0,0 +1,158 @@ +import os + +import pre_commit.constants as C +from pre_commit import git +from pre_commit.commands.autoupdate import autoupdate +from pre_commit.commands.gc import gc +from pre_commit.repository import all_hooks +from testing.fixtures import make_config_from_repo +from testing.fixtures import make_repo +from testing.fixtures import modify_config +from testing.fixtures import sample_local_config +from testing.fixtures import sample_meta_config +from testing.fixtures import write_config +from testing.util import git_commit + + +def _repo_count(store): + return len(store.select_all_repos()) + + +def _config_count(store): + return len(store.select_all_configs()) + + +def _remove_config_assert_cleared(store, cap_out): + os.remove(C.CONFIG_FILE) + assert not gc(store) + assert _config_count(store) == 0 + assert _repo_count(store) == 0 + assert cap_out.get().splitlines()[-1] == '1 repo(s) removed.' + + +def test_gc(tempdir_factory, store, in_git_dir, cap_out): + path = make_repo(tempdir_factory, 'script_hooks_repo') + old_rev = git.head_rev(path) + git_commit(cwd=path) + + write_config('.', make_config_from_repo(path, rev=old_rev)) + store.mark_config_used(C.CONFIG_FILE) + + # update will clone both the old and new repo, making the old one gc-able + assert not autoupdate(C.CONFIG_FILE, store, tags_only=False) + + assert _config_count(store) == 1 + assert _repo_count(store) == 2 + assert not gc(store) + assert _config_count(store) == 1 + assert _repo_count(store) == 1 + assert cap_out.get().splitlines()[-1] == '1 repo(s) removed.' + + _remove_config_assert_cleared(store, cap_out) + + +def test_gc_repo_not_cloned(tempdir_factory, store, in_git_dir, cap_out): + path = make_repo(tempdir_factory, 'script_hooks_repo') + write_config('.', make_config_from_repo(path)) + store.mark_config_used(C.CONFIG_FILE) + + assert _config_count(store) == 1 + assert _repo_count(store) == 0 + assert not gc(store) + assert _config_count(store) == 1 + assert _repo_count(store) == 0 + assert cap_out.get().splitlines()[-1] == '0 repo(s) removed.' + + +def test_gc_meta_repo_does_not_crash(store, in_git_dir, cap_out): + write_config('.', sample_meta_config()) + store.mark_config_used(C.CONFIG_FILE) + assert not gc(store) + assert cap_out.get().splitlines()[-1] == '0 repo(s) removed.' + + +def test_gc_local_repo_does_not_crash(store, in_git_dir, cap_out): + write_config('.', sample_local_config()) + store.mark_config_used(C.CONFIG_FILE) + assert not gc(store) + assert cap_out.get().splitlines()[-1] == '0 repo(s) removed.' + + +def test_gc_unused_local_repo_with_env(store, in_git_dir, cap_out): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'flake8', 'name': 'flake8', 'entry': 'flake8', + # a `language: python` local hook will create an environment + 'types': ['python'], 'language': 'python', + }], + } + write_config('.', config) + store.mark_config_used(C.CONFIG_FILE) + + # this causes the repositories to be created + all_hooks({'repos': [config]}, store) + + assert _config_count(store) == 1 + assert _repo_count(store) == 1 + assert not gc(store) + assert _config_count(store) == 1 + assert _repo_count(store) == 1 + assert cap_out.get().splitlines()[-1] == '0 repo(s) removed.' + + _remove_config_assert_cleared(store, cap_out) + + +def test_gc_config_with_missing_hook( + tempdir_factory, store, in_git_dir, cap_out, +): + path = make_repo(tempdir_factory, 'script_hooks_repo') + write_config('.', make_config_from_repo(path)) + store.mark_config_used(C.CONFIG_FILE) + + with modify_config() as config: + # just to trigger a clone + all_hooks(config, store) + # add a hook which does not exist, make sure we don't crash + config['repos'][0]['hooks'].append({'id': 'does-not-exist'}) + + assert _config_count(store) == 1 + assert _repo_count(store) == 1 + assert not gc(store) + assert _config_count(store) == 1 + assert _repo_count(store) == 1 + assert cap_out.get().splitlines()[-1] == '0 repo(s) removed.' + + _remove_config_assert_cleared(store, cap_out) + + +def test_gc_deletes_invalid_configs(store, in_git_dir, cap_out): + config = {'i am': 'invalid'} + write_config('.', config) + store.mark_config_used(C.CONFIG_FILE) + + assert _config_count(store) == 1 + assert not gc(store) + assert _config_count(store) == 0 + assert cap_out.get().splitlines()[-1] == '0 repo(s) removed.' + + +def test_invalid_manifest_gcd(tempdir_factory, store, in_git_dir, cap_out): + # clean up repos from old pre-commit versions + path = make_repo(tempdir_factory, 'script_hooks_repo') + write_config('.', make_config_from_repo(path)) + store.mark_config_used(C.CONFIG_FILE) + + # trigger a clone + assert not autoupdate(C.CONFIG_FILE, store, tags_only=False) + + # we'll "break" the manifest to simulate an old version clone + (_, _, path), = store.select_all_repos() + os.remove(os.path.join(path, C.MANIFEST_FILE)) + + assert _config_count(store) == 1 + assert _repo_count(store) == 1 + assert not gc(store) + assert _config_count(store) == 1 + assert _repo_count(store) == 0 + assert cap_out.get().splitlines()[-1] == '1 repo(s) removed.' diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 84ab1b2c8..2426068a2 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -22,6 +22,7 @@ from testing.fixtures import make_consuming_repo from testing.fixtures import modify_config from testing.fixtures import read_config +from testing.fixtures import sample_meta_config from testing.util import cmd_output_mocked_pre_commit_home from testing.util import cwd from testing.util import git_commit @@ -656,11 +657,7 @@ def test_pcre_deprecation_warning(cap_out, store, repo_with_passing_hook): def test_meta_hook_passes(cap_out, store, repo_with_passing_hook): - config = { - 'repo': 'meta', - 'hooks': [{'id': 'check-useless-excludes'}], - } - add_config_to_repo(repo_with_passing_hook, config) + add_config_to_repo(repo_with_passing_hook, sample_meta_config()) _test_run( cap_out, diff --git a/tests/main_test.py b/tests/main_test.py index c5db3da16..e5573b88d 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -65,8 +65,8 @@ def test_adjust_args_try_repo_repo_relative(in_git_dir): FNS = ( - 'autoupdate', 'clean', 'install', 'install_hooks', 'migrate_config', 'run', - 'sample_config', 'uninstall', + 'autoupdate', 'clean', 'gc', 'install', 'install_hooks', 'migrate_config', + 'run', 'sample_config', 'uninstall', ) CMDS = tuple(fn.replace('_', '-') for fn in FNS) diff --git a/tests/store_test.py b/tests/store_test.py index 8ef10a932..238343fda 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -12,7 +12,6 @@ from pre_commit import git from pre_commit.store import _get_default_directory from pre_commit.store import Store -from pre_commit.util import rmtree from testing.fixtures import git_dir from testing.util import cwd from testing.util import git_commit @@ -48,9 +47,7 @@ def test_uses_environment_variable_when_present(): assert ret == '/tmp/pre_commit_home' -def test_store_require_created(store): - assert not os.path.exists(store.directory) - store.require_created() +def test_store_init(store): # Should create the store directory assert os.path.exists(store.directory) # Should create a README file indicating what the directory is about @@ -63,30 +60,6 @@ def test_store_require_created(store): assert text_line in readme_contents -def test_store_require_created_does_not_create_twice(store): - assert not os.path.exists(store.directory) - store.require_created() - # We intentionally delete the directory here so we can figure out if it - # calls it again. - rmtree(store.directory) - assert not os.path.exists(store.directory) - # Call require_created, this should not trigger a call to create - store.require_created() - assert not os.path.exists(store.directory) - - -def test_does_not_recreate_if_directory_already_exists(store): - assert not os.path.exists(store.directory) - # We manually create the directory. - # Note: we're intentionally leaving out the README file. This is so we can - # know that `Store` didn't call create - os.mkdir(store.directory) - open(store.db_path, 'a').close() - # Call require_created, this should not call create - store.require_created() - assert not os.path.exists(os.path.join(store.directory, 'README')) - - def test_clone(store, tempdir_factory, log_info_mock): path = git_dir(tempdir_factory) with cwd(path): @@ -110,34 +83,25 @@ def test_clone(store, tempdir_factory, log_info_mock): assert git.head_rev(ret) == rev # Assert there's an entry in the sqlite db for this - with sqlite3.connect(store.db_path) as db: - path, = db.execute( - 'SELECT path from repos WHERE repo = ? and ref = ?', - (path, rev), - ).fetchone() - assert path == ret + assert store.select_all_repos() == [(path, rev, ret)] def test_clone_cleans_up_on_checkout_failure(store): - try: + with pytest.raises(Exception) as excinfo: # This raises an exception because you can't clone something that # doesn't exist! store.clone('/i_dont_exist_lol', 'fake_rev') - except Exception as e: - assert '/i_dont_exist_lol' in six.text_type(e) + assert '/i_dont_exist_lol' in six.text_type(excinfo.value) - things_starting_with_repo = [ - thing for thing in os.listdir(store.directory) - if thing.startswith('repo') + repo_dirs = [ + d for d in os.listdir(store.directory) if d.startswith('repo') ] - assert things_starting_with_repo == [] + assert repo_dirs == [] def test_clone_when_repo_already_exists(store): # Create an entry in the sqlite db that makes it look like the repo has # been cloned. - store.require_created() - with sqlite3.connect(store.db_path) as db: db.execute( 'INSERT INTO repos (repo, ref, path) ' @@ -147,14 +111,24 @@ def test_clone_when_repo_already_exists(store): assert store.clone('fake_repo', 'fake_ref') == 'fake_path' -def test_require_created_when_directory_exists_but_not_db(store): +def test_create_when_directory_exists_but_not_db(store): # In versions <= 0.3.5, there was no sqlite db causing a need for # backward compatibility - os.makedirs(store.directory) - store.require_created() + os.remove(store.db_path) + store = Store(store.directory) assert os.path.exists(store.db_path) +def test_create_when_store_already_exists(store): + # an assertion that this is idempotent and does not crash + Store(store.directory) + + +def test_db_repo_name(store): + assert store.db_repo_name('repo', ()) == 'repo' + assert store.db_repo_name('repo', ('b', 'a', 'c')) == 'repo:a,b,c' + + def test_local_resources_reflects_reality(): on_disk = { res[len('empty_template_'):] @@ -162,3 +136,35 @@ def test_local_resources_reflects_reality(): if res.startswith('empty_template_') } assert on_disk == set(Store.LOCAL_RESOURCES) + + +def test_mark_config_as_used(store, tmpdir): + with tmpdir.as_cwd(): + f = tmpdir.join('f').ensure() + store.mark_config_used('f') + assert store.select_all_configs() == [f.strpath] + + +def test_mark_config_as_used_idempotent(store, tmpdir): + test_mark_config_as_used(store, tmpdir) + test_mark_config_as_used(store, tmpdir) + + +def test_mark_config_as_used_does_not_exist(store): + store.mark_config_used('f') + assert store.select_all_configs() == [] + + +def _simulate_pre_1_14_0(store): + with store.connect() as db: + db.executescript('DROP TABLE configs') + + +def test_select_all_configs_roll_forward(store): + _simulate_pre_1_14_0(store) + assert store.select_all_configs() == [] + + +def test_mark_config_as_used_roll_forward(store, tmpdir): + _simulate_pre_1_14_0(store) + test_mark_config_as_used(store, tmpdir) From fc8456792346060dac053c535a9ce99aad43259e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 4 Jan 2019 21:43:08 -0800 Subject: [PATCH 322/544] Default local / meta through cfgv --- pre_commit/clientlib.py | 83 +++++++++++++++++-- pre_commit/meta_hooks/check_hooks_apply.py | 9 -- .../meta_hooks/check_useless_excludes.py | 9 -- pre_commit/meta_hooks/helpers.py | 10 --- pre_commit/meta_hooks/identity.py | 9 -- pre_commit/repository.py | 58 ++----------- setup.py | 2 +- tests/clientlib_test.py | 17 ++++ tests/commands/autoupdate_test.py | 6 +- tests/commands/gc_test.py | 3 +- tests/repository_test.py | 15 +--- 11 files changed, 109 insertions(+), 112 deletions(-) delete mode 100644 pre_commit/meta_hooks/helpers.py diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 07423c34d..c5b99477d 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -3,6 +3,8 @@ import argparse import functools +import pipes +import sys import cfgv from aspy.yaml import ordered_load @@ -88,8 +90,8 @@ def validate_manifest_main(argv=None): return ret -_LOCAL_SENTINEL = 'local' -_META_SENTINEL = 'meta' +_LOCAL = 'local' +_META = 'meta' class MigrateShaToRev(object): @@ -98,12 +100,12 @@ def _cond(key): return cfgv.Conditional( key, cfgv.check_string, condition_key='repo', - condition_value=cfgv.NotIn(_LOCAL_SENTINEL, _META_SENTINEL), + condition_value=cfgv.NotIn(_LOCAL, _META), ensure_absent=True, ) def check(self, dct): - if dct.get('repo') in {_LOCAL_SENTINEL, _META_SENTINEL}: + if dct.get('repo') in {_LOCAL, _META}: self._cond('rev').check(dct) self._cond('sha').check(dct) elif 'sha' in dct and 'rev' in dct: @@ -121,6 +123,61 @@ def remove_default(self, dct): pass +def _entry(modname): + """the hook `entry` is passed through `shlex.split()` by the command + runner, so to prevent issues with spaces and backslashes (on Windows) + it must be quoted here. + """ + return '{} -m pre_commit.meta_hooks.{}'.format( + pipes.quote(sys.executable), modname, + ) + + +_meta = ( + ( + 'check-hooks-apply', ( + ('name', 'Check hooks apply to the repository'), + ('files', C.CONFIG_FILE), + ('entry', _entry('check_hooks_apply')), + ), + ), + ( + 'check-useless-excludes', ( + ('name', 'Check for useless excludes'), + ('files', C.CONFIG_FILE), + ('entry', _entry('check_useless_excludes')), + ), + ), + ( + 'identity', ( + ('name', 'identity'), + ('verbose', True), + ('entry', _entry('identity')), + ), + ), +) + +META_HOOK_DICT = cfgv.Map( + 'Hook', 'id', + *([ + cfgv.Required('id', cfgv.check_string), + cfgv.Required('id', cfgv.check_one_of(tuple(k for k, _ in _meta))), + # language must be system + cfgv.Optional('language', cfgv.check_one_of({'system'}), 'system'), + ] + [ + # default to the hook definition for the meta hooks + cfgv.ConditionalOptional(key, cfgv.check_any, value, 'id', hook_id) + for hook_id, values in _meta + for key, value in values + ] + [ + # default to the "manifest" parsing + cfgv.OptionalNoDefault(item.key, item.check_fn) + # these will always be defaulted above + if item.key in {'name', 'language', 'entry'} else + item + for item in MANIFEST_HOOK_DICT.items + ]) +) CONFIG_HOOK_DICT = cfgv.Map( 'Hook', 'id', @@ -140,7 +197,19 @@ def remove_default(self, dct): 'Repository', 'repo', cfgv.Required('repo', cfgv.check_string), - cfgv.RequiredRecurse('hooks', cfgv.Array(CONFIG_HOOK_DICT)), + + cfgv.ConditionalRecurse( + 'hooks', cfgv.Array(CONFIG_HOOK_DICT), + 'repo', cfgv.NotIn(_LOCAL, _META), + ), + cfgv.ConditionalRecurse( + 'hooks', cfgv.Array(MANIFEST_HOOK_DICT), + 'repo', _LOCAL, + ), + cfgv.ConditionalRecurse( + 'hooks', cfgv.Array(META_HOOK_DICT), + 'repo', _META, + ), MigrateShaToRev(), ) @@ -154,11 +223,11 @@ def remove_default(self, dct): def is_local_repo(repo_entry): - return repo_entry['repo'] == _LOCAL_SENTINEL + return repo_entry['repo'] == _LOCAL def is_meta_repo(repo_entry): - return repo_entry['repo'] == _META_SENTINEL + return repo_entry['repo'] == _META class InvalidConfigError(FatalError): diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index a97830d20..b17a9d6f2 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -5,18 +5,9 @@ from pre_commit.clientlib import load_config from pre_commit.commands.run import _filter_by_include_exclude from pre_commit.commands.run import _filter_by_types -from pre_commit.meta_hooks.helpers import make_meta_entry from pre_commit.repository import all_hooks from pre_commit.store import Store -HOOK_DICT = { - 'id': 'check-hooks-apply', - 'name': 'Check hooks apply to the repository', - 'files': C.CONFIG_FILE, - 'language': 'system', - 'entry': make_meta_entry(__name__), -} - def check_all_hooks_match_files(config_file): files = git.get_all_files() diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 7918eb31c..18b9f1637 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -10,15 +10,6 @@ from pre_commit.clientlib import load_config from pre_commit.clientlib import MANIFEST_HOOK_DICT from pre_commit.commands.run import _filter_by_types -from pre_commit.meta_hooks.helpers import make_meta_entry - -HOOK_DICT = { - 'id': 'check-useless-excludes', - 'name': 'Check for useless excludes', - 'files': C.CONFIG_FILE, - 'language': 'system', - 'entry': make_meta_entry(__name__), -} def exclude_matches_any(filenames, include, exclude): diff --git a/pre_commit/meta_hooks/helpers.py b/pre_commit/meta_hooks/helpers.py deleted file mode 100644 index 7ef74861e..000000000 --- a/pre_commit/meta_hooks/helpers.py +++ /dev/null @@ -1,10 +0,0 @@ -import pipes -import sys - - -def make_meta_entry(modname): - """the hook `entry` is passed through `shlex.split()` by the command - runner, so to prevent issues with spaces and backslashes (on Windows) - it must be quoted here. - """ - return '{} -m {}'.format(pipes.quote(sys.executable), modname) diff --git a/pre_commit/meta_hooks/identity.py b/pre_commit/meta_hooks/identity.py index 0cec32a0c..ae7377b80 100644 --- a/pre_commit/meta_hooks/identity.py +++ b/pre_commit/meta_hooks/identity.py @@ -1,15 +1,6 @@ import sys from pre_commit import output -from pre_commit.meta_hooks.helpers import make_meta_entry - -HOOK_DICT = { - 'id': 'identity', - 'name': 'identity', - 'language': 'system', - 'verbose': True, - 'entry': make_meta_entry(__name__), -} def main(argv=None): diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 7b980928c..a654d0822 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -6,9 +6,6 @@ import logging import os -from cfgv import apply_defaults -from cfgv import validate - import pre_commit.constants as C from pre_commit import five from pre_commit.clientlib import is_local_repo @@ -137,15 +134,8 @@ def _hook(*hook_dicts): return ret -def _hook_from_manifest_dct(dct): - dct = apply_defaults(dct, MANIFEST_HOOK_DICT) - dct = validate(dct, MANIFEST_HOOK_DICT) - dct = _hook(dct) - return dct - - -def _local_repository_hooks(repo_config, store): - def _local_prefix(language_name, deps): +def _non_cloned_repository_hooks(repo_config, store): + def _prefix(language_name, deps): language = languages[language_name] # pcre / pygrep / script / system / docker_image do not have # environments so they work out of the current directory @@ -154,45 +144,11 @@ def _local_prefix(language_name, deps): else: return Prefix(store.make_local(deps)) - hook_dcts = [_hook_from_manifest_dct(h) for h in repo_config['hooks']] - return tuple( - Hook.create( - repo_config['repo'], - _local_prefix(hook['language'], hook['additional_dependencies']), - hook, - ) - for hook in hook_dcts - ) - - -def _meta_repository_hooks(repo_config, store): - # imported here to prevent circular imports. - from pre_commit.meta_hooks import check_hooks_apply - from pre_commit.meta_hooks import check_useless_excludes - from pre_commit.meta_hooks import identity - - meta_hooks = [ - _hook_from_manifest_dct(mod.HOOK_DICT) - for mod in (check_hooks_apply, check_useless_excludes, identity) - ] - by_id = {hook['id']: hook for hook in meta_hooks} - - for hook in repo_config['hooks']: - if hook['id'] not in by_id: - logger.error( - '`{}` is not a valid meta hook. ' - 'Typo? Perhaps it is introduced in a newer version? ' - 'Often `pip install --upgrade pre-commit` fixes this.' - .format(hook['id']), - ) - exit(1) - - prefix = Prefix(os.getcwd()) return tuple( Hook.create( repo_config['repo'], - prefix, - _hook(by_id[hook['id']], hook), + _prefix(hook['language'], hook['additional_dependencies']), + _hook(hook), ) for hook in repo_config['hooks'] ) @@ -225,10 +181,8 @@ def _cloned_repository_hooks(repo_config, store): def repository_hooks(repo_config, store): - if is_local_repo(repo_config): - return _local_repository_hooks(repo_config, store) - elif is_meta_repo(repo_config): - return _meta_repository_hooks(repo_config, store) + if is_local_repo(repo_config) or is_meta_repo(repo_config): + return _non_cloned_repository_hooks(repo_config, store) else: return _cloned_repository_hooks(repo_config, store) diff --git a/setup.py b/setup.py index f6ea719cf..6cc52d108 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ }, install_requires=[ 'aspy.yaml', - 'cfgv>=1.0.0', + 'cfgv>=1.3.0', 'identify>=1.0.0', # if this makes it into python3.8 move to extras_require 'importlib-metadata', diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index dbae4aad0..1f691c2bf 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -5,6 +5,7 @@ from pre_commit.clientlib import check_type_tag from pre_commit.clientlib import CONFIG_HOOK_DICT +from pre_commit.clientlib import CONFIG_REPO_DICT from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import is_local_repo from pre_commit.clientlib import MANIFEST_SCHEMA @@ -236,3 +237,19 @@ def test_migrate_to_sha_ok(): dct = {'repo': 'a', 'rev': 'b'} MigrateShaToRev().apply_default(dct) assert dct == {'repo': 'a', 'rev': 'b'} + + +@pytest.mark.parametrize( + 'config_repo', + ( + # i-dont-exist isn't a valid hook + {'repo': 'meta', 'hooks': [{'id': 'i-dont-exist'}]}, + # invalid to set a language for a meta hook + {'repo': 'meta', 'hooks': [{'id': 'identity', 'language': 'python'}]}, + # name override must be string + {'repo': 'meta', 'hooks': [{'id': 'identity', 'name': False}]}, + ), +) +def test_meta_hook_invalid_id(config_repo): + with pytest.raises(cfgv.ValidationError): + cfgv.validate(config_repo, CONFIG_REPO_DICT) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 8daf986aa..df7cb085c 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -8,7 +8,6 @@ import pre_commit.constants as C from pre_commit import git -from pre_commit.clientlib import load_config from pre_commit.commands.autoupdate import _update_repo from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.autoupdate import RepositoryCannotBeUpdatedError @@ -17,6 +16,7 @@ from testing.fixtures import add_config_to_repo from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo +from testing.fixtures import read_config from testing.fixtures import sample_local_config from testing.fixtures import write_config from testing.util import get_resource_path @@ -319,7 +319,7 @@ def test_autoupdate_local_hooks(in_git_dir, store): config = sample_local_config() add_config_to_repo('.', config) assert autoupdate(C.CONFIG_FILE, store, tags_only=False) == 0 - new_config_writen = load_config(C.CONFIG_FILE) + new_config_writen = read_config('.') assert len(new_config_writen['repos']) == 1 assert new_config_writen['repos'][0] == config @@ -334,7 +334,7 @@ def test_autoupdate_local_hooks_with_out_of_date_repo( config = {'repos': [local_config, stale_config]} write_config('.', config) assert autoupdate(C.CONFIG_FILE, store, tags_only=False) == 0 - new_config_writen = load_config(C.CONFIG_FILE) + new_config_writen = read_config('.') assert len(new_config_writen['repos']) == 2 assert new_config_writen['repos'][0] == local_config diff --git a/tests/commands/gc_test.py b/tests/commands/gc_test.py index 2f958f67f..2a0185093 100644 --- a/tests/commands/gc_test.py +++ b/tests/commands/gc_test.py @@ -2,6 +2,7 @@ import pre_commit.constants as C from pre_commit import git +from pre_commit.clientlib import load_config from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.gc import gc from pre_commit.repository import all_hooks @@ -91,7 +92,7 @@ def test_gc_unused_local_repo_with_env(store, in_git_dir, cap_out): store.mark_config_used(C.CONFIG_FILE) # this causes the repositories to be created - all_hooks({'repos': [config]}, store) + all_hooks(load_config(C.CONFIG_FILE), store) assert _config_count(store) == 1 assert _repo_count(store) == 1 diff --git a/tests/repository_test.py b/tests/repository_test.py index eecf67b6a..25fe24470 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -5,12 +5,14 @@ import re import shutil +import cfgv import mock import pytest import pre_commit.constants as C from pre_commit import five from pre_commit import parse_shebang +from pre_commit.clientlib import CONFIG_REPO_DICT from pre_commit.clientlib import load_manifest from pre_commit.languages import golang from pre_commit.languages import helpers @@ -42,6 +44,8 @@ def _norm_out(b): def _get_hook(config, store, hook_id): + config = cfgv.validate(config, CONFIG_REPO_DICT) + config = cfgv.apply_defaults(config, CONFIG_REPO_DICT) hooks = repository_hooks(config, store) install_hook_envs(hooks, store) hook, = [hook for hook in hooks if hook.id == hook_id] @@ -711,17 +715,6 @@ def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): ) -def test_meta_hook_not_present(store, fake_log_handler): - config = {'repo': 'meta', 'hooks': [{'id': 'i-dont-exist'}]} - with pytest.raises(SystemExit): - _get_hook(config, store, 'i-dont-exist') - assert fake_log_handler.handle.call_args[0][0].msg == ( - '`i-dont-exist` is not a valid meta hook. ' - 'Typo? Perhaps it is introduced in a newer version? ' - 'Often `pip install --upgrade pre-commit` fixes this.' - ) - - def test_too_new_version(tempdir_factory, store, fake_log_handler): path = make_repo(tempdir_factory, 'script_hooks_repo') with modify_manifest(path) as manifest: From d3b5a41830acc7254ae6c9bacc7895da5252e6a1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 5 Jan 2019 13:01:42 -0800 Subject: [PATCH 323/544] Implement default_language_version --- .pre-commit-config.yaml | 2 +- pre_commit/clientlib.py | 41 ++++++++-------- pre_commit/commands/autoupdate.py | 7 ++- pre_commit/commands/gc.py | 8 ++-- pre_commit/commands/install_uninstall.py | 3 +- pre_commit/constants.py | 2 + pre_commit/languages/all.py | 3 +- pre_commit/languages/docker.py | 3 +- pre_commit/languages/golang.py | 5 +- pre_commit/languages/helpers.py | 6 +-- pre_commit/languages/node.py | 3 +- pre_commit/languages/python.py | 5 +- pre_commit/languages/ruby.py | 11 +++-- pre_commit/languages/rust.py | 5 +- pre_commit/languages/swift.py | 5 +- pre_commit/repository.py | 47 ++++++++++--------- pre_commit/xargs.py | 2 +- tests/clientlib_test.py | 22 ++++++--- tests/commands/gc_test.py | 4 +- tests/commands/install_uninstall_test.py | 4 +- tests/languages/helpers_test.py | 3 +- tests/repository_test.py | 60 +++++++++++++++++------- tests/xargs_test.py | 2 +- 23 files changed, 150 insertions(+), 103 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4fe852f77..9ffdbe942 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: rev: v1.3.0 hooks: - id: reorder-python-imports - language_version: python2.7 + language_version: python3 - repo: https://github.com/asottile/add-trailing-comma rev: v0.7.1 hooks: diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index c5b99477d..d458daef7 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -55,7 +55,7 @@ def _make_argparser(filenames_help): cfgv.Optional('always_run', cfgv.check_bool, False), cfgv.Optional('pass_filenames', cfgv.check_bool, True), cfgv.Optional('description', cfgv.check_string, ''), - cfgv.Optional('language_version', cfgv.check_string, 'default'), + cfgv.Optional('language_version', cfgv.check_string, C.DEFAULT), cfgv.Optional('log_file', cfgv.check_string, ''), cfgv.Optional('minimum_pre_commit_version', cfgv.check_string, '0'), cfgv.Optional('require_serial', cfgv.check_bool, False), @@ -90,8 +90,8 @@ def validate_manifest_main(argv=None): return ret -_LOCAL = 'local' -_META = 'meta' +LOCAL = 'local' +META = 'meta' class MigrateShaToRev(object): @@ -100,12 +100,12 @@ def _cond(key): return cfgv.Conditional( key, cfgv.check_string, condition_key='repo', - condition_value=cfgv.NotIn(_LOCAL, _META), + condition_value=cfgv.NotIn(LOCAL, META), ensure_absent=True, ) def check(self, dct): - if dct.get('repo') in {_LOCAL, _META}: + if dct.get('repo') in {LOCAL, META}: self._cond('rev').check(dct) self._cond('sha').check(dct) elif 'sha' in dct and 'rev' in dct: @@ -159,12 +159,11 @@ def _entry(modname): META_HOOK_DICT = cfgv.Map( 'Hook', 'id', + cfgv.Required('id', cfgv.check_string), + cfgv.Required('id', cfgv.check_one_of(tuple(k for k, _ in _meta))), + # language must be system + cfgv.Optional('language', cfgv.check_one_of({'system'}), 'system'), *([ - cfgv.Required('id', cfgv.check_string), - cfgv.Required('id', cfgv.check_one_of(tuple(k for k, _ in _meta))), - # language must be system - cfgv.Optional('language', cfgv.check_one_of({'system'}), 'system'), - ] + [ # default to the hook definition for the meta hooks cfgv.ConditionalOptional(key, cfgv.check_any, value, 'id', hook_id) for hook_id, values in _meta @@ -200,36 +199,36 @@ def _entry(modname): cfgv.ConditionalRecurse( 'hooks', cfgv.Array(CONFIG_HOOK_DICT), - 'repo', cfgv.NotIn(_LOCAL, _META), + 'repo', cfgv.NotIn(LOCAL, META), ), cfgv.ConditionalRecurse( 'hooks', cfgv.Array(MANIFEST_HOOK_DICT), - 'repo', _LOCAL, + 'repo', LOCAL, ), cfgv.ConditionalRecurse( 'hooks', cfgv.Array(META_HOOK_DICT), - 'repo', _META, + 'repo', META, ), MigrateShaToRev(), ) +DEFAULT_LANGUAGE_VERSION = cfgv.Map( + 'DefaultLanguageVersion', None, + cfgv.NoAdditionalKeys(all_languages), + *[cfgv.Optional(x, cfgv.check_string, C.DEFAULT) for x in all_languages] +) CONFIG_SCHEMA = cfgv.Map( 'Config', None, cfgv.RequiredRecurse('repos', cfgv.Array(CONFIG_REPO_DICT)), + cfgv.OptionalRecurse( + 'default_language_version', DEFAULT_LANGUAGE_VERSION, {}, + ), cfgv.Optional('exclude', cfgv.check_regex, '^$'), cfgv.Optional('fail_fast', cfgv.check_bool, False), ) -def is_local_repo(repo_entry): - return repo_entry['repo'] == _LOCAL - - -def is_meta_repo(repo_entry): - return repo_entry['repo'] == _META - - class InvalidConfigError(FatalError): pass diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index f75a19242..99e96050d 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -13,10 +13,10 @@ from pre_commit import output from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import InvalidManifestError -from pre_commit.clientlib import is_local_repo -from pre_commit.clientlib import is_meta_repo from pre_commit.clientlib import load_config from pre_commit.clientlib import load_manifest +from pre_commit.clientlib import LOCAL +from pre_commit.clientlib import META from pre_commit.commands.migrate_config import migrate_config from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output @@ -123,8 +123,7 @@ def autoupdate(config_file, store, tags_only, repos=()): for repo_config in input_config['repos']: if ( - is_local_repo(repo_config) or - is_meta_repo(repo_config) or + repo_config['repo'] in {LOCAL, META} or # Skip updating any repo_configs that aren't for the specified repo repos and repo_config['repo'] not in repos ): diff --git a/pre_commit/commands/gc.py b/pre_commit/commands/gc.py index 9722643d3..65818e50e 100644 --- a/pre_commit/commands/gc.py +++ b/pre_commit/commands/gc.py @@ -7,16 +7,16 @@ from pre_commit import output from pre_commit.clientlib import InvalidConfigError from pre_commit.clientlib import InvalidManifestError -from pre_commit.clientlib import is_local_repo -from pre_commit.clientlib import is_meta_repo from pre_commit.clientlib import load_config from pre_commit.clientlib import load_manifest +from pre_commit.clientlib import LOCAL +from pre_commit.clientlib import META def _mark_used_repos(store, all_repos, unused_repos, repo): - if is_meta_repo(repo): + if repo['repo'] == META: return - elif is_local_repo(repo): + elif repo['repo'] == LOCAL: for hook in repo['hooks']: deps = hook.get('additional_dependencies') unused_repos.discard(( diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index a5df93126..4ff2a413f 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -6,6 +6,7 @@ import os.path import sys +import pre_commit.constants as C from pre_commit import git from pre_commit import output from pre_commit.clientlib import load_config @@ -51,7 +52,7 @@ def shebang(): py = 'python' else: py = python.get_default_version() - if py == 'default': + if py == C.DEFAULT: py = 'python' return '#!/usr/bin/env {}'.format(py) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index a8cdc2e5c..996480a9a 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -22,3 +22,5 @@ # `manual` is not invoked by any installed git hook. See #719 STAGES = ('commit', 'commit-msg', 'manual', 'push') + +DEFAULT = 'default' diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index fecce4713..6d85ddf1f 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -35,8 +35,7 @@ # # Args: # prefix - `Prefix` bound to the repository. -# version - A version specified in the hook configuration or -# 'default'. +# version - A version specified in the hook configuration or 'default'. # """ # # def run_hook(hook, file_args): diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index e5f3a36b8..59a53b4fb 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -4,6 +4,7 @@ import hashlib import os +import pre_commit.constants as C from pre_commit import five from pre_commit.languages import helpers from pre_commit.util import CalledProcessError @@ -62,7 +63,7 @@ def install_environment( assert_docker_available() directory = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, 'default'), + helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), ) # Docker doesn't really have relevant disk environment, but pre-commit diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 92d5d36ce..e19df88ae 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -4,6 +4,7 @@ import os.path import sys +import pre_commit.constants as C from pre_commit import git from pre_commit.envcontext import envcontext from pre_commit.envcontext import Var @@ -27,7 +28,7 @@ def get_env_patch(venv): @contextlib.contextmanager def in_env(prefix): envdir = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, 'default'), + helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), ) with envcontext(get_env_patch(envdir)): yield @@ -52,7 +53,7 @@ def guess_go_dir(remote_url): def install_environment(prefix, version, additional_dependencies): helpers.assert_version_default('golang', version) directory = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, 'default'), + helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), ) with clean_path_on_failure(directory): diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index faff14379..0915f4105 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -7,10 +7,10 @@ import six +import pre_commit.constants as C from pre_commit.util import cmd_output from pre_commit.xargs import xargs - FIXED_RANDOM_SEED = 1542676186 @@ -30,7 +30,7 @@ def to_cmd(hook): def assert_version_default(binary, version): - if version != 'default': + if version != C.DEFAULT: raise AssertionError( 'For now, pre-commit requires system-installed {}'.format(binary), ) @@ -45,7 +45,7 @@ def assert_no_additional_deps(lang, additional_deps): def basic_get_default_version(): - return 'default' + return C.DEFAULT def basic_healthy(prefix, language_version): diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 07f785eaf..b313bf5b5 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -4,6 +4,7 @@ import os import sys +import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import Var from pre_commit.languages import helpers @@ -57,7 +58,7 @@ def install_environment(prefix, version, additional_dependencies): cmd = [ sys.executable, '-mnodeenv', '--prebuilt', '--clean-src', envdir, ] - if version != 'default': + if version != C.DEFAULT: cmd.extend(['-n', version]) cmd_output(*cmd) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index fab5450a6..46aa05957 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -4,6 +4,7 @@ import os import sys +import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var @@ -76,7 +77,7 @@ def _norm(path): return exe # We tried! - return 'default' + return C.DEFAULT def get_default_version(): @@ -134,7 +135,7 @@ def install_environment(prefix, version, additional_dependencies): env_dir = prefix.path(directory) with clean_path_on_failure(env_dir): - if version != 'default': + if version != C.DEFAULT: python = norm_version(version) else: python = os.path.realpath(sys.executable) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 04a74155b..c721b3ceb 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -6,6 +6,7 @@ import shutil import tarfile +import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import Var from pre_commit.languages import helpers @@ -32,7 +33,7 @@ def get_env_patch(venv, language_version): # pragma: windows no cover ), ), ) - if language_version != 'default': + if language_version != C.DEFAULT: patches += (('RBENV_VERSION', language_version),) return patches @@ -52,14 +53,14 @@ def _extract_resource(filename, dest): tf.extractall(dest) -def _install_rbenv(prefix, version='default'): # pragma: windows no cover +def _install_rbenv(prefix, version=C.DEFAULT): # pragma: windows no cover directory = helpers.environment_dir(ENVIRONMENT_DIR, version) _extract_resource('rbenv.tar.gz', prefix.path('.')) shutil.move(prefix.path('rbenv'), prefix.path(directory)) # Only install ruby-build if the version is specified - if version != 'default': + if version != C.DEFAULT: plugins_dir = prefix.path(directory, 'plugins') _extract_resource('ruby-download.tar.gz', plugins_dir) _extract_resource('ruby-build.tar.gz', plugins_dir) @@ -84,7 +85,7 @@ def _install_rbenv(prefix, version='default'): # pragma: windows no cover ) # If we aren't using the system ruby, add a version here - if version != 'default': + if version != C.DEFAULT: activate_file.write('export RBENV_VERSION="{}"\n'.format(version)) @@ -109,7 +110,7 @@ def install_environment( # Need to call this before installing so rbenv's directories are # set up helpers.run_setup_cmd(prefix, ('rbenv', 'init', '-')) - if version != 'default': + if version != C.DEFAULT: _install_ruby(prefix, version) # Need to call this after installing to set up the shims helpers.run_setup_cmd(prefix, ('rbenv', 'rehash')) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index e81fbad26..e09d0078f 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -5,6 +5,7 @@ import toml +import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import Var from pre_commit.languages import helpers @@ -29,7 +30,7 @@ def get_env_patch(target_dir): @contextlib.contextmanager def in_env(prefix): target_dir = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, 'default'), + helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), ) with envcontext(get_env_patch(target_dir)): yield @@ -50,7 +51,7 @@ def _add_dependencies(cargo_toml_path, additional_dependencies): def install_environment(prefix, version, additional_dependencies): helpers.assert_version_default('rust', version) directory = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, 'default'), + helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), ) # There are two cases where we might want to specify more dependencies: diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 5841f25e5..3f5a92f14 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -3,6 +3,7 @@ import contextlib import os +import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import Var from pre_commit.languages import helpers @@ -24,7 +25,7 @@ def get_env_patch(venv): # pragma: windows no cover @contextlib.contextmanager def in_env(prefix): # pragma: windows no cover envdir = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, 'default'), + helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), ) with envcontext(get_env_patch(envdir)): yield @@ -36,7 +37,7 @@ def install_environment( helpers.assert_version_default('swift', version) helpers.assert_no_additional_deps('swift', additional_dependencies) directory = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, 'default'), + helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), ) # Build the swift package diff --git a/pre_commit/repository.py b/pre_commit/repository.py index a654d0822..76001fa1f 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -8,10 +8,10 @@ import pre_commit.constants as C from pre_commit import five -from pre_commit.clientlib import is_local_repo -from pre_commit.clientlib import is_meta_repo from pre_commit.clientlib import load_manifest +from pre_commit.clientlib import LOCAL from pre_commit.clientlib import MANIFEST_HOOK_DICT +from pre_commit.clientlib import META from pre_commit.languages.all import languages from pre_commit.languages.helpers import environment_dir from pre_commit.prefix import Prefix @@ -111,7 +111,9 @@ def create(cls, src, prefix, dct): return cls(src=src, prefix=prefix, **{k: dct[k] for k in _KEYS}) -def _hook(*hook_dicts): +def _hook(*hook_dicts, **kwargs): + root_config = kwargs.pop('root_config') + assert not kwargs, kwargs ret, rest = dict(hook_dicts[0]), hook_dicts[1:] for dct in rest: ret.update(dct) @@ -127,14 +129,16 @@ def _hook(*hook_dicts): ) exit(1) - if ret['language_version'] == 'default': - language = languages[ret['language']] - ret['language_version'] = language.get_default_version() + lang = ret['language'] + if ret['language_version'] == C.DEFAULT: + ret['language_version'] = root_config['default_language_version'][lang] + if ret['language_version'] == C.DEFAULT: + ret['language_version'] = languages[lang].get_default_version() return ret -def _non_cloned_repository_hooks(repo_config, store): +def _non_cloned_repository_hooks(repo_config, store, root_config): def _prefix(language_name, deps): language = languages[language_name] # pcre / pygrep / script / system / docker_image do not have @@ -148,13 +152,13 @@ def _prefix(language_name, deps): Hook.create( repo_config['repo'], _prefix(hook['language'], hook['additional_dependencies']), - _hook(hook), + _hook(hook, root_config=root_config), ) for hook in repo_config['hooks'] ) -def _cloned_repository_hooks(repo_config, store): +def _cloned_repository_hooks(repo_config, store, root_config): repo, rev = repo_config['repo'], repo_config['rev'] manifest_path = os.path.join(store.clone(repo, rev), C.MANIFEST_FILE) by_id = {hook['id']: hook for hook in load_manifest(manifest_path)} @@ -169,7 +173,10 @@ def _cloned_repository_hooks(repo_config, store): ) exit(1) - hook_dcts = [_hook(by_id[h['id']], h) for h in repo_config['hooks']] + hook_dcts = [ + _hook(by_id[hook['id']], hook, root_config=root_config) + for hook in repo_config['hooks'] + ] return tuple( Hook.create( repo_config['repo'], @@ -180,11 +187,11 @@ def _cloned_repository_hooks(repo_config, store): ) -def repository_hooks(repo_config, store): - if is_local_repo(repo_config) or is_meta_repo(repo_config): - return _non_cloned_repository_hooks(repo_config, store) +def _repository_hooks(repo_config, store, root_config): + if repo_config['repo'] in {LOCAL, META}: + return _non_cloned_repository_hooks(repo_config, store, root_config) else: - return _cloned_repository_hooks(repo_config, store) + return _cloned_repository_hooks(repo_config, store, root_config) def install_hook_envs(hooks, store): @@ -201,17 +208,13 @@ def _need_installed(): return with store.exclusive_lock(): # Another process may have already completed this work - need_installed = _need_installed() - if not need_installed: # pragma: no cover (race) - return - - for hook in need_installed: + for hook in _need_installed(): hook.install() -def all_hooks(config, store): +def all_hooks(root_config, store): return tuple( hook - for repo in config['repos'] - for hook in repository_hooks(repo, store) + for repo in root_config['repos'] + for hook in _repository_hooks(repo, store, root_config) ) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 3b4a25f9c..e2686f0f9 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -2,11 +2,11 @@ from __future__ import division from __future__ import unicode_literals +import concurrent.futures import contextlib import math import sys -import concurrent.futures import six from pre_commit import parse_shebang diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 1f691c2bf..fd7f051a0 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -7,7 +7,7 @@ from pre_commit.clientlib import CONFIG_HOOK_DICT from pre_commit.clientlib import CONFIG_REPO_DICT from pre_commit.clientlib import CONFIG_SCHEMA -from pre_commit.clientlib import is_local_repo +from pre_commit.clientlib import DEFAULT_LANGUAGE_VERSION from pre_commit.clientlib import MANIFEST_SCHEMA from pre_commit.clientlib import MigrateShaToRev from pre_commit.clientlib import validate_config_main @@ -30,10 +30,6 @@ def test_check_type_tag_failures(value): check_type_tag(value) -def test_is_local_repo(): - assert is_local_repo({'repo': 'local'}) - - @pytest.mark.parametrize( ('args', 'expected_output'), ( @@ -250,6 +246,20 @@ def test_migrate_to_sha_ok(): {'repo': 'meta', 'hooks': [{'id': 'identity', 'name': False}]}, ), ) -def test_meta_hook_invalid_id(config_repo): +def test_meta_hook_invalid(config_repo): with pytest.raises(cfgv.ValidationError): cfgv.validate(config_repo, CONFIG_REPO_DICT) + + +@pytest.mark.parametrize( + 'mapping', + ( + # invalid language key + {'pony': '1.0'}, + # not a string for version + {'python': 3}, + ), +) +def test_default_language_version_invalid(mapping): + with pytest.raises(cfgv.ValidationError): + cfgv.validate(mapping, DEFAULT_LANGUAGE_VERSION) diff --git a/tests/commands/gc_test.py b/tests/commands/gc_test.py index 2a0185093..d2528507e 100644 --- a/tests/commands/gc_test.py +++ b/tests/commands/gc_test.py @@ -110,10 +110,10 @@ def test_gc_config_with_missing_hook( path = make_repo(tempdir_factory, 'script_hooks_repo') write_config('.', make_config_from_repo(path)) store.mark_config_used(C.CONFIG_FILE) + # to trigger a clone + all_hooks(load_config(C.CONFIG_FILE), store) with modify_config() as config: - # just to trigger a clone - all_hooks(config, store) # add a hook which does not exist, make sure we don't crash config['repos'][0]['hooks'].append({'id': 'does-not-exist'}) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 2faa19178..608fe3856 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -53,13 +53,13 @@ def test_shebang_windows(): def test_shebang_otherwise(): with mock.patch.object(sys, 'platform', 'posix'): - assert 'default' not in shebang() + assert C.DEFAULT not in shebang() def test_shebang_returns_default(): with mock.patch.object(sys, 'platform', 'posix'): with mock.patch.object( - python, 'get_default_version', return_value='default', + python, 'get_default_version', return_value=C.DEFAULT, ): assert shebang() == '#!/usr/bin/env python' diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index 831e0d598..629322c37 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -8,6 +8,7 @@ import mock import pytest +import pre_commit.constants as C from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError @@ -15,7 +16,7 @@ def test_basic_get_default_version(): - assert helpers.basic_get_default_version() == 'default' + assert helpers.basic_get_default_version() == C.DEFAULT def test_basic_healthy(): diff --git a/tests/repository_test.py b/tests/repository_test.py index 25fe24470..d237da2bb 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -12,7 +12,7 @@ import pre_commit.constants as C from pre_commit import five from pre_commit import parse_shebang -from pre_commit.clientlib import CONFIG_REPO_DICT +from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest from pre_commit.languages import golang from pre_commit.languages import helpers @@ -22,9 +22,9 @@ from pre_commit.languages import ruby from pre_commit.languages import rust from pre_commit.prefix import Prefix +from pre_commit.repository import all_hooks from pre_commit.repository import Hook from pre_commit.repository import install_hook_envs -from pre_commit.repository import repository_hooks from pre_commit.util import cmd_output from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo @@ -43,15 +43,21 @@ def _norm_out(b): return b.replace(b'\r\n', b'\n') -def _get_hook(config, store, hook_id): - config = cfgv.validate(config, CONFIG_REPO_DICT) - config = cfgv.apply_defaults(config, CONFIG_REPO_DICT) - hooks = repository_hooks(config, store) - install_hook_envs(hooks, store) +def _get_hook_no_install(repo_config, store, hook_id): + config = {'repos': [repo_config]} + config = cfgv.validate(config, CONFIG_SCHEMA) + config = cfgv.apply_defaults(config, CONFIG_SCHEMA) + hooks = all_hooks(config, store) hook, = [hook for hook in hooks if hook.id == hook_id] return hook +def _get_hook(repo_config, store, hook_id): + hook = _get_hook_no_install(repo_config, store, hook_id) + install_hook_envs([hook], store) + return hook + + def _test_hook_repo( tempdir_factory, store, @@ -81,7 +87,7 @@ def test_python_hook_default_version(tempdir_factory, store): # make sure that this continues to work for platforms where default # language detection does not work with mock.patch.object( - python, 'get_default_version', return_value='default', + python, 'get_default_version', return_value=C.DEFAULT, ): test_python_hook(tempdir_factory, store) @@ -278,7 +284,7 @@ def test_additional_rust_cli_dependencies_installed( config['hooks'][0]['additional_dependencies'] = [dep] hook = _get_hook(config, store, 'rust-hook') binaries = os.listdir(hook.prefix.path( - helpers.environment_dir(rust.ENVIRONMENT_DIR, 'default'), 'bin', + helpers.environment_dir(rust.ENVIRONMENT_DIR, C.DEFAULT), 'bin', )) # normalize for windows binaries = [os.path.splitext(binary)[0] for binary in binaries] @@ -295,7 +301,7 @@ def test_additional_rust_lib_dependencies_installed( config['hooks'][0]['additional_dependencies'] = deps hook = _get_hook(config, store, 'rust-hook') binaries = os.listdir(hook.prefix.path( - helpers.environment_dir(rust.ENVIRONMENT_DIR, 'default'), 'bin', + helpers.environment_dir(rust.ENVIRONMENT_DIR, C.DEFAULT), 'bin', )) # normalize for windows binaries = [os.path.splitext(binary)[0] for binary in binaries] @@ -494,7 +500,7 @@ def test_additional_golang_dependencies_installed( config['hooks'][0]['additional_dependencies'] = deps hook = _get_hook(config, store, 'golang-hook') binaries = os.listdir(hook.prefix.path( - helpers.environment_dir(golang.ENVIRONMENT_DIR, 'default'), 'bin', + helpers.environment_dir(golang.ENVIRONMENT_DIR, C.DEFAULT), 'bin', )) # normalize for windows binaries = [os.path.splitext(binary)[0] for binary in binaries] @@ -588,7 +594,7 @@ def test_control_c_control_c_on_install(tempdir_factory, store): """Regression test for #186.""" path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) - hooks = repository_hooks(config, store) + hooks = [_get_hook_no_install(config, store, 'foo')] class MyKeyboardInterrupt(KeyboardInterrupt): pass @@ -686,22 +692,42 @@ def test_tags_on_repositories(in_tmpdir, tempdir_factory, store): assert ret2[1] == b'bar\nHello World\n' -def test_local_python_repo(store): +@pytest.fixture +def local_python_config(): # Make a "local" hooks repo that just installs our other hooks repo repo_path = get_resource_path('python_hooks_repo') manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) hooks = [ dict(hook, additional_dependencies=[repo_path]) for hook in manifest ] - config = {'repo': 'local', 'hooks': hooks} - hook = _get_hook(config, store, 'foo') + return {'repo': 'local', 'hooks': hooks} + + +def test_local_python_repo(store, local_python_config): + hook = _get_hook(local_python_config, store, 'foo') # language_version should have been adjusted to the interpreter version - assert hook.language_version != 'default' + assert hook.language_version != C.DEFAULT ret = hook.run(('filename',)) assert ret[0] == 0 assert _norm_out(ret[1]) == b"['filename']\nHello World\n" +def test_default_language_version(store, local_python_config): + config = { + 'default_language_version': {'python': 'fake'}, + 'repos': [local_python_config], + } + + # `language_version` was not set, should default + hook, = all_hooks(config, store) + assert hook.language_version == 'fake' + + # `language_version` is set, should not default + config['repos'][0]['hooks'][0]['language_version'] = 'fake2' + hook, = all_hooks(config, store) + assert hook.language_version == 'fake2' + + def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): path = make_repo(tempdir_factory, 'script_hooks_repo') config = make_config_from_repo(path) @@ -760,7 +786,7 @@ def test_manifest_hooks(tempdir_factory, store): files='', id='bash_hook', language='script', - language_version='default', + language_version=C.DEFAULT, log_file='', minimum_pre_commit_version='0', name='Bash hook', diff --git a/tests/xargs_test.py b/tests/xargs_test.py index ed65ed462..0e91f9be8 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -2,10 +2,10 @@ from __future__ import absolute_import from __future__ import unicode_literals +import concurrent.futures import sys import time -import concurrent.futures import mock import pytest import six From bd65d8947fbe546b3b57b4342c9efd6d975b0ae3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 6 Jan 2019 09:54:55 -0800 Subject: [PATCH 324/544] Implement default_stages --- pre_commit/clientlib.py | 5 +++++ pre_commit/commands/run.py | 2 +- pre_commit/repository.py | 3 +++ tests/repository_test.py | 22 ++++++++++++++++++++-- 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index d458daef7..77b92d4bf 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -224,6 +224,11 @@ def _entry(modname): cfgv.OptionalRecurse( 'default_language_version', DEFAULT_LANGUAGE_VERSION, {}, ), + cfgv.Optional( + 'default_stages', + cfgv.check_array(cfgv.check_one_of(C.STAGES)), + C.STAGES, + ), cfgv.Optional('exclude', cfgv.check_regex, '^$'), cfgv.Optional('fail_fast', cfgv.check_bool, False), ) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index f38b25c72..97d56b8d3 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -252,7 +252,7 @@ def run(config_file, store, args, environ=os.environ): hook for hook in all_hooks(config, store) if not args.hook or hook.id == args.hook or hook.alias == args.hook - if not hook.stages or args.hook_stage in hook.stages + if args.hook_stage in hook.stages ] if args.hook and not hooks: diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 76001fa1f..1d92d7531 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -135,6 +135,9 @@ def _hook(*hook_dicts, **kwargs): if ret['language_version'] == C.DEFAULT: ret['language_version'] = languages[lang].get_default_version() + if not ret['stages']: + ret['stages'] = root_config['default_stages'] + return ret diff --git a/tests/repository_test.py b/tests/repository_test.py index d237da2bb..590e7f25a 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -715,6 +715,7 @@ def test_local_python_repo(store, local_python_config): def test_default_language_version(store, local_python_config): config = { 'default_language_version': {'python': 'fake'}, + 'default_stages': ['commit'], 'repos': [local_python_config], } @@ -728,6 +729,23 @@ def test_default_language_version(store, local_python_config): assert hook.language_version == 'fake2' +def test_default_stages(store, local_python_config): + config = { + 'default_language_version': {'python': C.DEFAULT}, + 'default_stages': ['commit'], + 'repos': [local_python_config], + } + + # `stages` was not set, should default + hook, = all_hooks(config, store) + assert hook.stages == ['commit'] + + # `stages` is set, should not default + config['repos'][0]['hooks'][0]['stages'] = ['push'] + hook, = all_hooks(config, store) + assert hook.stages == ['push'] + + def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): path = make_repo(tempdir_factory, 'script_hooks_repo') config = make_config_from_repo(path) @@ -786,13 +804,13 @@ def test_manifest_hooks(tempdir_factory, store): files='', id='bash_hook', language='script', - language_version=C.DEFAULT, + language_version='default', log_file='', minimum_pre_commit_version='0', name='Bash hook', pass_filenames=True, require_serial=False, - stages=[], + stages=('commit', 'commit-msg', 'manual', 'push'), types=['file'], verbose=False, ) From bea33af31024b086b9abee2a5dc7dcdc626f9fda Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 6 Jan 2019 11:52:22 -0800 Subject: [PATCH 325/544] small cleanups in tests --- testing/resources/manifest_without_foo.yaml | 5 -- .../valid_yaml_but_invalid_config.yaml | 5 -- .../valid_yaml_but_invalid_manifest.yaml | 1 - tests/clientlib_test.py | 83 ++++++------------- tests/commands/autoupdate_test.py | 12 +-- tests/commands/migrate_config_test.py | 2 - 6 files changed, 30 insertions(+), 78 deletions(-) delete mode 100644 testing/resources/manifest_without_foo.yaml delete mode 100644 testing/resources/valid_yaml_but_invalid_config.yaml delete mode 100644 testing/resources/valid_yaml_but_invalid_manifest.yaml diff --git a/testing/resources/manifest_without_foo.yaml b/testing/resources/manifest_without_foo.yaml deleted file mode 100644 index 0220233aa..000000000 --- a/testing/resources/manifest_without_foo.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: bar - name: Bar - entry: bar - language: python - files: \.py$ diff --git a/testing/resources/valid_yaml_but_invalid_config.yaml b/testing/resources/valid_yaml_but_invalid_config.yaml deleted file mode 100644 index 2ed187b2d..000000000 --- a/testing/resources/valid_yaml_but_invalid_config.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- repo: git@github.com:pre-commit/pre-commit-hooks - hooks: - - id: pyflakes - - id: jslint - - id: trim_trailing_whitespace diff --git a/testing/resources/valid_yaml_but_invalid_manifest.yaml b/testing/resources/valid_yaml_but_invalid_manifest.yaml deleted file mode 100644 index 20e9ff3fe..000000000 --- a/testing/resources/valid_yaml_but_invalid_manifest.yaml +++ /dev/null @@ -1 +0,0 @@ -foo: bar diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index fd7f051a0..839bcaf9f 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -13,7 +13,6 @@ from pre_commit.clientlib import validate_config_main from pre_commit.clientlib import validate_manifest_main from testing.fixtures import sample_local_config -from testing.util import get_resource_path def is_valid_according_to_schema(obj, obj_schema): @@ -30,19 +29,6 @@ def test_check_type_tag_failures(value): check_type_tag(value) -@pytest.mark.parametrize( - ('args', 'expected_output'), - ( - (['.pre-commit-config.yaml'], 0), - (['non_existent_file.yaml'], 1), - ([get_resource_path('valid_yaml_but_invalid_config.yaml')], 1), - ([get_resource_path('non_parseable_yaml_file.notyaml')], 1), - ), -) -def test_validate_config_main(args, expected_output): - assert validate_config_main(args) == expected_output - - @pytest.mark.parametrize( ('config_obj', 'expected'), ( ( @@ -91,39 +77,13 @@ def test_config_valid(config_obj, expected): def test_local_hooks_with_rev_fails(): - config_obj = {'repos': [sample_local_config()]} - config_obj['repos'][0]['rev'] = 'foo' + config_obj = {'repos': [dict(sample_local_config(), rev='foo')]} with pytest.raises(cfgv.ValidationError): cfgv.validate(config_obj, CONFIG_SCHEMA) -@pytest.mark.parametrize( - 'config_obj', ( - {'repos': [{ - 'repo': 'local', - 'hooks': [{ - 'id': 'arg-per-line', - 'name': 'Args per line hook', - 'entry': 'bin/hook.sh', - 'language': 'script', - 'files': '', - 'args': ['hello', 'world'], - }], - }]}, - {'repos': [{ - 'repo': 'local', - 'hooks': [{ - 'id': 'arg-per-line', - 'name': 'Args per line hook', - 'entry': 'bin/hook.sh', - 'language': 'script', - 'files': '', - 'args': ['hello', 'world'], - }], - }]}, - ), -) -def test_config_with_local_hooks_definition_passes(config_obj): +def test_config_with_local_hooks_definition_passes(): + config_obj = {'repos': [sample_local_config()]} cfgv.validate(config_obj, CONFIG_SCHEMA) @@ -135,17 +95,30 @@ def test_config_schema_does_not_contain_defaults(): assert not isinstance(item, cfgv.Optional) -@pytest.mark.parametrize( - ('args', 'expected_output'), - ( - (['.pre-commit-hooks.yaml'], 0), - (['non_existent_file.yaml'], 1), - ([get_resource_path('valid_yaml_but_invalid_manifest.yaml')], 1), - ([get_resource_path('non_parseable_yaml_file.notyaml')], 1), - ), -) -def test_validate_manifest_main(args, expected_output): - assert validate_manifest_main(args) == expected_output +def test_validate_manifest_main_ok(): + assert not validate_manifest_main(('.pre-commit-hooks.yaml',)) + + +def test_validate_config_main_ok(): + assert not validate_config_main(('.pre-commit-config.yaml',)) + + +def test_validate_config_old_list_format_ok(tmpdir): + f = tmpdir.join('cfg.yaml') + f.write('- {repo: meta, hooks: [{id: identity}]}') + assert not validate_config_main((f.strpath,)) + + +@pytest.mark.parametrize('fn', (validate_config_main, validate_manifest_main)) +def test_mains_not_ok(tmpdir, fn): + not_yaml = tmpdir.join('f.notyaml') + not_yaml.write('{') + not_schema = tmpdir.join('notconfig.yaml') + not_schema.write('{}') + + assert fn(('does-not-exist',)) + assert fn((not_yaml.strpath,)) + assert fn((not_schema.strpath,)) @pytest.mark.parametrize( @@ -174,8 +147,6 @@ def test_validate_manifest_main(args, expected_output): ), ( # A regression in 0.13.5: always_run and files are permissible - # together (but meaningless). In a future version upgrade this to - # an error [{ 'id': 'a', 'name': 'b', diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index df7cb085c..c1fceb42e 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -1,8 +1,6 @@ from __future__ import unicode_literals -import os.path import pipes -import shutil import pytest @@ -16,10 +14,10 @@ from testing.fixtures import add_config_to_repo from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo +from testing.fixtures import modify_manifest from testing.fixtures import read_config from testing.fixtures import sample_local_config from testing.fixtures import write_config -from testing.util import get_resource_path from testing.util import git_commit @@ -275,12 +273,8 @@ def hook_disappearing_repo(tempdir_factory): path = make_repo(tempdir_factory, 'python_hooks_repo') original_rev = git.head_rev(path) - shutil.copy( - get_resource_path('manifest_without_foo.yaml'), - os.path.join(path, C.MANIFEST_FILE), - ) - cmd_output('git', 'add', '.', cwd=path) - git_commit(cwd=path) + with modify_manifest(path) as manifest: + manifest[0]['id'] = 'bar' yield auto_namedtuple(path=path, original_rev=original_rev) diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index da599f10a..8f9153fdc 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -129,7 +129,6 @@ def test_migrate_config_sha_to_rev(tmpdir): '- repo: https://github.com/pre-commit/pre-commit-hooks\n' ' sha: v1.2.0\n' ' hooks: []\n' - 'repos:\n' '- repo: https://github.com/pre-commit/pre-commit-hooks\n' ' sha: v1.2.0\n' ' hooks: []\n' @@ -144,7 +143,6 @@ def test_migrate_config_sha_to_rev(tmpdir): '- repo: https://github.com/pre-commit/pre-commit-hooks\n' ' rev: v1.2.0\n' ' hooks: []\n' - 'repos:\n' '- repo: https://github.com/pre-commit/pre-commit-hooks\n' ' rev: v1.2.0\n' ' hooks: []\n' From 8432d9b692efcb5544edca51ec2a3128e1fdb734 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 7 Jan 2019 07:38:16 -0800 Subject: [PATCH 326/544] bump cfgv, forgot in last PR --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6cc52d108..0963c4aaf 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ }, install_requires=[ 'aspy.yaml', - 'cfgv>=1.3.0', + 'cfgv>=1.4.0', 'identify>=1.0.0', # if this makes it into python3.8 move to extras_require 'importlib-metadata', From e60579d9f3475fb483f51217b8c1840752a5e86c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 13 Dec 2018 10:08:57 -0800 Subject: [PATCH 327/544] Fix staged-files-only for `git add --intent-to-add` files --- pre_commit/git.py | 14 ++++++++++++++ pre_commit/staged_files_only.py | 32 +++++++++++++++++++++++++++----- tests/git_test.py | 18 ++++++++++++++++++ tests/staged_files_only_test.py | 12 ++++++++++++ 4 files changed, 71 insertions(+), 5 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index ccdd18566..f0b504043 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -97,6 +97,20 @@ def get_staged_files(): )[1]) +def intent_to_add_files(): + _, stdout_binary, _ = cmd_output('git', 'status', '--porcelain', '-z') + parts = list(reversed(zsplit(stdout_binary))) + intent_to_add = [] + while parts: + line = parts.pop() + status, filename = line[:3], line[3:] + if status[0] in {'C', 'R'}: # renames / moves have an additional arg + parts.pop() + if status[1] == 'A': + intent_to_add.append(filename) + return intent_to_add + + def get_all_files(): return zsplit(cmd_output('git', 'ls-files', '-z')[1]) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 1d0c36488..7af319d72 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -6,9 +6,11 @@ import os.path import time +from pre_commit import git from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import mkdirp +from pre_commit.xargs import xargs logger = logging.getLogger('pre_commit') @@ -24,11 +26,22 @@ def _git_apply(patch): @contextlib.contextmanager -def staged_files_only(patch_dir): - """Clear any unstaged changes from the git working directory inside this - context. - """ - # Determine if there are unstaged files +def _intent_to_add_cleared(): + intent_to_add = git.intent_to_add_files() + if intent_to_add: + logger.warning('Unstaged intent-to-add files detected.') + + xargs(('git', 'rm', '--cached', '--'), intent_to_add) + try: + yield + finally: + xargs(('git', 'add', '--intent-to-add', '--'), intent_to_add) + else: + yield + + +@contextlib.contextmanager +def _unstaged_changes_cleared(patch_dir): tree = cmd_output('git', 'write-tree')[1].strip() retcode, diff_stdout_binary, _ = cmd_output( 'git', 'diff-index', '--ignore-submodules', '--binary', @@ -71,3 +84,12 @@ def staged_files_only(patch_dir): # There weren't any staged files so we don't need to do anything # special yield + + +@contextlib.contextmanager +def staged_files_only(patch_dir): + """Clear any unstaged changes from the git working directory inside this + context. + """ + with _intent_to_add_cleared(), _unstaged_changes_cleared(patch_dir): + yield diff --git a/tests/git_test.py b/tests/git_test.py index a78b74581..43f1c1569 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -155,3 +155,21 @@ def test_get_conflicted_files_non_ascii(in_merge_conflict): cmd_output('git', 'add', '.') ret = git.get_conflicted_files() assert ret == {'conflict_file', 'интервью'} + + +def test_intent_to_add(in_git_dir): + in_git_dir.join('a').ensure() + cmd_output('git', 'add', '--intent-to-add', 'a') + + assert git.intent_to_add_files() == ['a'] + + +def test_status_output_with_rename(in_git_dir): + in_git_dir.join('a').write('1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n') + cmd_output('git', 'add', 'a') + git_commit() + cmd_output('git', 'mv', 'a', 'b') + in_git_dir.join('c').ensure() + cmd_output('git', 'add', '--intent-to-add', 'c') + + assert git.intent_to_add_files() == ['c'] diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 619d739b1..2410bffec 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -9,6 +9,7 @@ import pytest +from pre_commit import git from pre_commit.staged_files_only import staged_files_only from pre_commit.util import cmd_output from testing.auto_namedtuple import auto_namedtuple @@ -339,3 +340,14 @@ def test_autocrlf_commited_crlf(in_git_dir, patch_dir): with staged_files_only(patch_dir): assert_no_diff() + + +def test_intent_to_add(in_git_dir, patch_dir): + """Regression test for #881""" + _write(b'hello\nworld\n') + cmd_output('git', 'add', '--intent-to-add', 'foo') + + assert git.intent_to_add_files() == ['foo'] + with staged_files_only(patch_dir): + assert_no_diff() + assert git.intent_to_add_files() == ['foo'] From 1cf4b54cba8726ef67840c807589a56dabbbd2cf Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 8 Jan 2019 10:57:44 -0800 Subject: [PATCH 328/544] v1.14.0 --- CHANGELOG.md | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f8fc775b..c73062b42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,61 @@ +1.14.0 +====== + +### Features +- Add an `alias` configuration value to allow repeated hooks to be + differentiated + - #882 issue by @s0undt3ch. + - #886 PR by @s0undt3ch. +- Add `identity` meta hook which just prints filenames + - #865 issue by @asottile. + - #898 PR by @asottile. +- Factor out `cached-property` and improve startup performance by ~10% + - #899 PR by @asottile. +- Add a warning on unexpected keys in configuration + - #899 PR by @asottile. +- Teach `pre-commit try-repo` to clone uncommitted changes on disk. + - #589 issue by @sverhagen. + - #703 issue by @asottile. + - #904 PR by @asottile. +- Implement `pre-commit gc` which will clean up no-longer-referenced cache + repos. + - #283 issue by @jtwang. + - #906 PR by @asottile. +- Add top level config `default_language_version` to streamline overriding the + `language_version` configuration in many places + - #647 issue by @asottile. + - #908 PR by @asottile. +- Add top level config `default_stages` to streamline overriding the `stages` + configuration in many places + - #768 issue by @mattlqx. + - #909 PR by @asottile. + +### Fixes +- More intelligently pick hook shebang (`#!/usr/bin/env python3`) + - #878 issue by @fristedt. + - #893 PR by @asottile. +- Several fixes related to `--files` / `--config`: + - `pre-commit run --files x` outside of a git dir no longer stacktraces + - `pre-commit run --config ./relative` while in a sub directory of the git + repo is now able to find the configuration + - `pre-commit run --files ...` no longer runs a subprocess per file + (performance) + - #895 PR by @asottile. +- `pre-commit try-repo ./relative` while in a sub directory of the git repo is + now able to clone properly + - #903 PR by @asottile. +- Ensure `meta` repos cannot have a language other than `system` + - #905 issue by @asottile. + - #907 PR by @asottile. +- Fix committing with unstaged files that were `git add --intent-to-add` added + - #881 issue by @henniss. + - #912 PR by @asottile. + +### Misc +- Use `--no-gpg-sign` when running tests + - #894 PR by @s0undt3ch. + + 1.13.0 ====== diff --git a/setup.py b/setup.py index 0963c4aaf..4d453b0e2 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.13.0', + version='1.14.0', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From 32d65236bf53701da4e09b9fd7aa05aeafe53633 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 10 Jan 2019 06:48:49 -0800 Subject: [PATCH 329/544] Use sys.executable if it matches the requested version --- pre_commit/languages/python.py | 18 ++++++++++++++++++ tests/languages/python_test.py | 16 ++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 46aa05957..86f5368cb 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -89,8 +89,26 @@ def get_default_version(): return get_default_version() +def _sys_executable_matches(version): + if version == 'python': + return True + elif not version.startswith('python'): + return False + + try: + info = tuple(int(p) for p in version[len('python'):].split('.')) + except ValueError: + return False + + return sys.version_info[:len(info)] == info + + def norm_version(version): if os.name == 'nt': # pragma: no cover (windows) + # first see if our current executable is appropriate + if _sys_executable_matches(version): + return sys.executable + # Try looking up by name version_exec = find_executable(version) if version_exec and version_exec != version: diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 366c010e6..426d3ec6f 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -2,6 +2,10 @@ from __future__ import unicode_literals import os.path +import sys + +import mock +import pytest from pre_commit.languages import python @@ -16,3 +20,15 @@ def test_norm_version_expanduser(): expected_path = home + '/.pyenv/versions/3.4.3/bin/python' result = python.norm_version(path) assert result == expected_path + + +@pytest.mark.parametrize('v', ('python3.6', 'python3', 'python')) +def test_sys_executable_matches(v): + with mock.patch.object(sys, 'version_info', (3, 6, 7)): + assert python._sys_executable_matches(v) + + +@pytest.mark.parametrize('v', ('notpython', 'python3.x')) +def test_sys_executable_matches_does_not_match(v): + with mock.patch.object(sys, 'version_info', (3, 6, 7)): + assert not python._sys_executable_matches(v) From cc1af1da06578d1fb94f15add7d5690b43fbde37 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 10 Jan 2019 10:21:36 -0800 Subject: [PATCH 330/544] v1.14.1 --- CHANGELOG.md | 8 ++++++++ setup.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c73062b42..7696dd70e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +1.14.1 +====== + +### Fixes +- Fix python executable lookup on windows when using conda + - #913 issue by @dawelter2. + - #914 PR by @asottile. + 1.14.0 ====== diff --git a/setup.py b/setup.py index 4d453b0e2..7e6a138f5 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.14.0', + version='1.14.1', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From 4f8a9580aa71ec47e7f55d41a4131a88fe0cd862 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 10 Jan 2019 14:26:55 -0800 Subject: [PATCH 331/544] Be more timid about choosing a shebang --- pre_commit/commands/install_uninstall.py | 18 ++++++++++++++---- tests/commands/install_uninstall_test.py | 17 +++++++++-------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 4ff2a413f..a6d501abe 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -2,15 +2,14 @@ from __future__ import unicode_literals import io +import itertools import logging import os.path import sys -import pre_commit.constants as C from pre_commit import git from pre_commit import output from pre_commit.clientlib import load_config -from pre_commit.languages import python from pre_commit.repository import all_hooks from pre_commit.repository import install_hook_envs from pre_commit.util import cmd_output @@ -51,8 +50,19 @@ def shebang(): if sys.platform == 'win32': py = 'python' else: - py = python.get_default_version() - if py == C.DEFAULT: + # Homebrew/homebrew-core#35825: be more timid about appropriate `PATH` + path_choices = [p for p in os.defpath.split(os.pathsep) if p] + exe_choices = [ + 'python{}'.format('.'.join( + str(v) for v in sys.version_info[:i] + )) + for i in range(3) + ] + for path, exe in itertools.product(path_choices, exe_choices): + if os.path.exists(os.path.join(path, exe)): + py = exe + break + else: py = 'python' return '#!/usr/bin/env {}'.format(py) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 608fe3856..c19aaa440 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -18,7 +18,6 @@ from pre_commit.commands.install_uninstall import PRIOR_HASHES from pre_commit.commands.install_uninstall import shebang from pre_commit.commands.install_uninstall import uninstall -from pre_commit.languages import python from pre_commit.util import cmd_output from pre_commit.util import make_executable from pre_commit.util import mkdirp @@ -51,17 +50,19 @@ def test_shebang_windows(): assert shebang() == '#!/usr/bin/env python' -def test_shebang_otherwise(): +def test_shebang_posix_not_on_path(): with mock.patch.object(sys, 'platform', 'posix'): - assert C.DEFAULT not in shebang() + with mock.patch.object(os, 'defpath', ''): + assert shebang() == '#!/usr/bin/env python' + +def test_shebang_posix_on_path(tmpdir): + tmpdir.join('python{}'.format(sys.version_info[0])).ensure() -def test_shebang_returns_default(): with mock.patch.object(sys, 'platform', 'posix'): - with mock.patch.object( - python, 'get_default_version', return_value=C.DEFAULT, - ): - assert shebang() == '#!/usr/bin/env python' + with mock.patch.object(os, 'defpath', tmpdir.strpath): + expected = '#!/usr/bin/env python{}'.format(sys.version_info[0]) + assert shebang() == expected def test_install_pre_commit(in_git_dir, store): From 90cfe677bc2c22f56064b9922b8cbf1ff12a1254 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 10 Jan 2019 16:04:07 -0800 Subject: [PATCH 332/544] v1.14.2 --- CHANGELOG.md | 8 ++++++++ setup.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7696dd70e..acd7f9969 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +1.14.2 +====== + +### Fixes +- Make the hook shebang detection more timid (1.14.0 regression) + - Homebrew/homebrew-core#35825. + - #915 PR by @asottile. + 1.14.1 ====== diff --git a/setup.py b/setup.py index 7e6a138f5..240a7e3e0 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.14.1', + version='1.14.2', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From 9898b490df02e5784e0230d2d87979c0875aeb70 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 11 Jan 2019 07:39:51 -0800 Subject: [PATCH 333/544] Fix non-parallel option changelog entry --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acd7f9969..13b1dd915 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,7 +77,7 @@ ### Features - Run hooks in parallel - - individual hooks may opt out of parallel exection with `parallel: false` + - individual hooks may opt out of parallel exection with `require_serial: true` - #510 issue by @chriskuehl. - #851 PR by @chriskuehl. @@ -103,7 +103,7 @@ - #885 PR by @s0undt3ch. ### Updating -- If a hook requires serial execution, set `parallel: false` to avoid the new +- If a hook requires serial execution, set `require_serial: true` to avoid the new parallel execution. - `ruby` hooks now require `gem>=2.0.0`. If your platform doesn't support this by default, select a newer version using From ea58596a56f49e16185ec1c33ed59852554265f9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 20 Jan 2019 22:22:39 -0800 Subject: [PATCH 334/544] Revert "Merge pull request #888 from pre-commit/887_xfail_windows_node_again" This reverts commit 45a34d6b7543563c3cda847428aebefde269310d, reversing changes made to d0c62aae7a93e3bd14eba7d131c15b4211201ef3. --- testing/util.py | 1 - 1 file changed, 1 deletion(-) diff --git a/testing/util.py b/testing/util.py index f0406089b..156967302 100644 --- a/testing/util.py +++ b/testing/util.py @@ -48,7 +48,6 @@ def cmd_output_mocked_pre_commit_home(*args, **kwargs): def broken_deep_listdir(): # pragma: no cover (platform specific) if sys.platform != 'win32': return False - return True # TODO: remove this after #887 is resolved try: os.listdir(str('\\\\?\\') + os.path.abspath(str('.'))) except OSError: From b1389603e0b56dc32381b3c7b3fd1e338651f8e5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 23 Jan 2019 20:42:27 -0800 Subject: [PATCH 335/544] Speed up filename filtering. Before there was a `getcwd` syscall for every filename which was filtered. Instead this is now cached per-run. - When all files are identified by filename only: ~45% improvement - When no files are identified by filename only: ~55% improvement This makes little difference to overall execution, the bigger win is eliminating the `memoize_by_cwd` hack. Just removing the memoization would have *increased* the runtime by 300-500%. --- pre_commit/commands/run.py | 71 +++++++++++-------- pre_commit/meta_hooks/check_hooks_apply.py | 11 +-- .../meta_hooks/check_useless_excludes.py | 11 +-- pre_commit/util.py | 18 ----- tests/commands/run_test.py | 19 +++-- tests/util_test.py | 34 --------- 6 files changed, 61 insertions(+), 103 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 97d56b8d3..651c7f3fd 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -17,14 +17,47 @@ from pre_commit.repository import install_hook_envs from pre_commit.staged_files_only import staged_files_only from pre_commit.util import cmd_output -from pre_commit.util import memoize_by_cwd from pre_commit.util import noop_context logger = logging.getLogger('pre_commit') -tags_from_path = memoize_by_cwd(tags_from_path) +def filter_by_include_exclude(names, include, exclude): + include_re, exclude_re = re.compile(include), re.compile(exclude) + return [ + filename for filename in names + if include_re.search(filename) + if not exclude_re.search(filename) + ] + + +class Classifier(object): + def __init__(self, filenames): + self.filenames = [f for f in filenames if os.path.lexists(f)] + self._types_cache = {} + + def _types_for_file(self, filename): + try: + return self._types_cache[filename] + except KeyError: + ret = self._types_cache[filename] = tags_from_path(filename) + return ret + + def by_types(self, names, types, exclude_types): + types, exclude_types = frozenset(types), frozenset(exclude_types) + ret = [] + for filename in names: + tags = self._types_for_file(filename) + if tags >= types and not tags & exclude_types: + ret.append(filename) + return ret + + def filenames_for_hook(self, hook): + names = self.filenames + names = filter_by_include_exclude(names, hook.files, hook.exclude) + names = self.by_types(names, hook.types, hook.exclude_types) + return names def _get_skips(environ): @@ -36,37 +69,12 @@ def _hook_msg_start(hook, verbose): return '{}{}'.format('[{}] '.format(hook.id) if verbose else '', hook.name) -def _filter_by_include_exclude(filenames, include, exclude): - include_re, exclude_re = re.compile(include), re.compile(exclude) - return [ - filename for filename in filenames - if ( - include_re.search(filename) and - not exclude_re.search(filename) and - os.path.lexists(filename) - ) - ] - - -def _filter_by_types(filenames, types, exclude_types): - types, exclude_types = frozenset(types), frozenset(exclude_types) - ret = [] - for filename in filenames: - tags = tags_from_path(filename) - if tags >= types and not tags & exclude_types: - ret.append(filename) - return tuple(ret) - - SKIPPED = 'Skipped' NO_FILES = '(no files to check)' -def _run_single_hook(filenames, hook, args, skips, cols): - include, exclude = hook.files, hook.exclude - filenames = _filter_by_include_exclude(filenames, include, exclude) - types, exclude_types = hook.types, hook.exclude_types - filenames = _filter_by_types(filenames, types, exclude_types) +def _run_single_hook(classifier, hook, args, skips, cols): + filenames = classifier.filenames_for_hook(hook) if hook.language == 'pcre': logger.warning( @@ -193,10 +201,11 @@ def _run_hooks(config, hooks, args, environ): skips = _get_skips(environ) cols = _compute_cols(hooks, args.verbose) filenames = _all_filenames(args) - filenames = _filter_by_include_exclude(filenames, '', config['exclude']) + filenames = filter_by_include_exclude(filenames, '', config['exclude']) + classifier = Classifier(filenames) retval = 0 for hook in hooks: - retval |= _run_single_hook(filenames, hook, args, skips, cols) + retval |= _run_single_hook(classifier, hook, args, skips, cols) if retval and config['fail_fast']: break if retval and args.show_diff_on_failure and git.has_diff(): diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index b17a9d6f2..b1ccdac3d 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -3,24 +3,19 @@ import pre_commit.constants as C from pre_commit import git from pre_commit.clientlib import load_config -from pre_commit.commands.run import _filter_by_include_exclude -from pre_commit.commands.run import _filter_by_types +from pre_commit.commands.run import Classifier from pre_commit.repository import all_hooks from pre_commit.store import Store def check_all_hooks_match_files(config_file): - files = git.get_all_files() + classifier = Classifier(git.get_all_files()) retv = 0 for hook in all_hooks(load_config(config_file), Store()): if hook.always_run or hook.language == 'fail': continue - include, exclude = hook.files, hook.exclude - filtered = _filter_by_include_exclude(files, include, exclude) - types, exclude_types = hook.types, hook.exclude_types - filtered = _filter_by_types(filtered, types, exclude_types) - if not filtered: + elif not classifier.filenames_for_hook(hook): print('{} does not apply to this repository'.format(hook.id)) retv = 1 diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 18b9f1637..c4860db33 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -9,7 +9,7 @@ from pre_commit import git from pre_commit.clientlib import load_config from pre_commit.clientlib import MANIFEST_HOOK_DICT -from pre_commit.commands.run import _filter_by_types +from pre_commit.commands.run import Classifier def exclude_matches_any(filenames, include, exclude): @@ -24,11 +24,11 @@ def exclude_matches_any(filenames, include, exclude): def check_useless_excludes(config_file): config = load_config(config_file) - files = git.get_all_files() + classifier = Classifier(git.get_all_files()) retv = 0 exclude = config['exclude'] - if not exclude_matches_any(files, '', exclude): + if not exclude_matches_any(classifier.filenames, '', exclude): print( 'The global exclude pattern {!r} does not match any files' .format(exclude), @@ -40,10 +40,11 @@ def check_useless_excludes(config_file): # Not actually a manifest dict, but this more accurately reflects # the defaults applied during runtime hook = apply_defaults(hook, MANIFEST_HOOK_DICT) + names = classifier.filenames types, exclude_types = hook['types'], hook['exclude_types'] - filtered_by_types = _filter_by_types(files, types, exclude_types) + names = classifier.by_types(names, types, exclude_types) include, exclude = hook['files'], hook['exclude'] - if not exclude_matches_any(filtered_by_types, include, exclude): + if not exclude_matches_any(names, include, exclude): print( 'The exclude pattern {!r} for {} does not match any files' .format(exclude, hook['id']), diff --git a/pre_commit/util.py b/pre_commit/util.py index c38af5a28..4c3902897 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -2,7 +2,6 @@ import contextlib import errno -import functools import os.path import shutil import stat @@ -31,23 +30,6 @@ def mkdirp(path): raise -def memoize_by_cwd(func): - """Memoize a function call based on os.getcwd().""" - @functools.wraps(func) - def wrapper(*args): - cwd = os.getcwd() - key = (cwd,) + args - try: - return wrapper._cache[key] - except KeyError: - ret = wrapper._cache[key] = func(*args) - return ret - - wrapper._cache = {} - - return wrapper - - @contextlib.contextmanager def clean_path_on_failure(path): """Cleans up the directory on an exceptional failure.""" diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 2426068a2..e37eca646 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -11,9 +11,10 @@ import pre_commit.constants as C from pre_commit.commands.install_uninstall import install from pre_commit.commands.run import _compute_cols -from pre_commit.commands.run import _filter_by_include_exclude from pre_commit.commands.run import _get_skips from pre_commit.commands.run import _has_unmerged_paths +from pre_commit.commands.run import Classifier +from pre_commit.commands.run import filter_by_include_exclude from pre_commit.commands.run import run from pre_commit.util import cmd_output from pre_commit.util import make_executable @@ -748,18 +749,22 @@ def test_fail_fast(cap_out, store, repo_with_failing_hook): assert printed.count(b'Failing hook') == 1 +def test_classifier_removes_dne(): + classifier = Classifier(('this_file_does_not_exist',)) + assert classifier.filenames == [] + + @pytest.fixture def some_filenames(): return ( '.pre-commit-hooks.yaml', - 'im_a_file_that_doesnt_exist.py', 'pre_commit/git.py', 'pre_commit/main.py', ) def test_include_exclude_base_case(some_filenames): - ret = _filter_by_include_exclude(some_filenames, '', '^$') + ret = filter_by_include_exclude(some_filenames, '', '^$') assert ret == [ '.pre-commit-hooks.yaml', 'pre_commit/git.py', @@ -771,22 +776,22 @@ def test_include_exclude_base_case(some_filenames): def test_matches_broken_symlink(tmpdir): with tmpdir.as_cwd(): os.symlink('does-not-exist', 'link') - ret = _filter_by_include_exclude({'link'}, '', '^$') + ret = filter_by_include_exclude({'link'}, '', '^$') assert ret == ['link'] def test_include_exclude_total_match(some_filenames): - ret = _filter_by_include_exclude(some_filenames, r'^.*\.py$', '^$') + ret = filter_by_include_exclude(some_filenames, r'^.*\.py$', '^$') assert ret == ['pre_commit/git.py', 'pre_commit/main.py'] def test_include_exclude_does_search_instead_of_match(some_filenames): - ret = _filter_by_include_exclude(some_filenames, r'\.yaml$', '^$') + ret = filter_by_include_exclude(some_filenames, r'\.yaml$', '^$') assert ret == ['.pre-commit-hooks.yaml'] def test_include_exclude_exclude_removes_files(some_filenames): - ret = _filter_by_include_exclude(some_filenames, '', r'\.py$') + ret = filter_by_include_exclude(some_filenames, '', r'\.py$') assert ret == ['.pre-commit-hooks.yaml'] diff --git a/tests/util_test.py b/tests/util_test.py index 56eb5aaa2..8178bb4bf 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -1,17 +1,14 @@ from __future__ import unicode_literals import os.path -import random import pytest from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output -from pre_commit.util import memoize_by_cwd from pre_commit.util import parse_version from pre_commit.util import tmpdir -from testing.util import cwd def test_CalledProcessError_str(): @@ -42,37 +39,6 @@ def test_CalledProcessError_str_nooutput(): ) -@pytest.fixture -def memoized_by_cwd(): - @memoize_by_cwd - def func(arg): - return arg + str(random.getrandbits(64)) - - return func - - -def test_memoized_by_cwd_returns_same_twice_in_a_row(memoized_by_cwd): - ret = memoized_by_cwd('baz') - ret2 = memoized_by_cwd('baz') - assert ret is ret2 - - -def test_memoized_by_cwd_returns_different_for_different_args(memoized_by_cwd): - ret = memoized_by_cwd('baz') - ret2 = memoized_by_cwd('bar') - assert ret.startswith('baz') - assert ret2.startswith('bar') - assert ret != ret2 - - -def test_memoized_by_cwd_changes_with_different_cwd(memoized_by_cwd): - ret = memoized_by_cwd('baz') - with cwd('.git'): - ret2 = memoized_by_cwd('baz') - - assert ret != ret2 - - def test_clean_on_failure_noop(in_tmpdir): with clean_path_on_failure('foo'): pass From fe5390c068dc0605100e35aa0141bcf6425c057e Mon Sep 17 00:00:00 2001 From: "Andrew S. Brown" Date: Sun, 27 Jan 2019 07:35:02 -0800 Subject: [PATCH 336/544] Ensure that GOBIN is not set when installing a golang hook If GOBIN is set, it will be used as the install path instead of the first item from GOPATH followed by "/bin". If it is used, commands will not be isolated between different repos. --- pre_commit/languages/golang.py | 3 +++ tests/repository_test.py | 13 ++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index e19df88ae..c28c469e7 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -7,6 +7,7 @@ import pre_commit.constants as C from pre_commit import git from pre_commit.envcontext import envcontext +from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure @@ -21,6 +22,7 @@ def get_env_patch(venv): return ( + ('GOBIN', UNSET), ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), ) @@ -69,6 +71,7 @@ def install_environment(prefix, version, additional_dependencies): else: gopath = directory env = dict(os.environ, GOPATH=gopath) + env.pop('GOBIN', None) cmd_output('go', 'get', './...', cwd=repo_src_dir, env=env) for dependency in additional_dependencies: cmd_output('go', 'get', dependency, cwd=repo_src_dir, env=env) diff --git a/tests/repository_test.py b/tests/repository_test.py index 590e7f25a..5acbfde51 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -14,6 +14,7 @@ from pre_commit import parse_shebang from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest +from pre_commit.envcontext import envcontext from pre_commit.languages import golang from pre_commit.languages import helpers from pre_commit.languages import node @@ -71,7 +72,7 @@ def _test_hook_repo( path = make_repo(tempdir_factory, repo_path) config = make_config_from_repo(path, **(config_kwargs or {})) ret = _get_hook(config, store, hook_id).run(args) - assert ret[0] == expected_return_code + assert ret[0] == expected_return_code, "output was: {}".format(ret[1]) assert _norm_out(ret[1]) == expected @@ -267,6 +268,16 @@ def test_golang_hook(tempdir_factory, store): ) +def test_golang_hook_still_works_when_gobin_is_set(tempdir_factory, store): + gobin_dir = tempdir_factory.get() + with envcontext([('GOBIN', gobin_dir)]): + _test_hook_repo( + tempdir_factory, store, 'golang_hooks_repo', + 'golang-hook', [], b'hello world\n', + ) + assert os.listdir(gobin_dir) == [], "hook should not be installed in $GOBIN" + + def test_rust_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'rust_hooks_repo', From 950bc2c7fb996b4a19393e116fc4a6fe57ad5d21 Mon Sep 17 00:00:00 2001 From: "Andrew S. Brown" Date: Sun, 27 Jan 2019 14:02:53 -0800 Subject: [PATCH 337/544] Shorten line --- tests/repository_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/repository_test.py b/tests/repository_test.py index 5acbfde51..282da235d 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -275,7 +275,7 @@ def test_golang_hook_still_works_when_gobin_is_set(tempdir_factory, store): tempdir_factory, store, 'golang_hooks_repo', 'golang-hook', [], b'hello world\n', ) - assert os.listdir(gobin_dir) == [], "hook should not be installed in $GOBIN" + assert os.listdir(gobin_dir) == [], "hook must not be installed in $GOBIN" def test_rust_hook(tempdir_factory, store): From 1eed1b51b871a853c9058c074f1a8c4d83b3b67f Mon Sep 17 00:00:00 2001 From: "Andrew S. Brown" Date: Sun, 27 Jan 2019 17:55:11 -0800 Subject: [PATCH 338/544] Address PR feedback --- pre_commit/languages/golang.py | 2 -- tests/repository_test.py | 9 +++------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index c28c469e7..f6124dd5d 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -7,7 +7,6 @@ import pre_commit.constants as C from pre_commit import git from pre_commit.envcontext import envcontext -from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure @@ -22,7 +21,6 @@ def get_env_patch(venv): return ( - ('GOBIN', UNSET), ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), ) diff --git a/tests/repository_test.py b/tests/repository_test.py index 282da235d..5f03a455a 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -72,7 +72,7 @@ def _test_hook_repo( path = make_repo(tempdir_factory, repo_path) config = make_config_from_repo(path, **(config_kwargs or {})) ret = _get_hook(config, store, hook_id).run(args) - assert ret[0] == expected_return_code, "output was: {}".format(ret[1]) + assert ret[0] == expected_return_code assert _norm_out(ret[1]) == expected @@ -271,11 +271,8 @@ def test_golang_hook(tempdir_factory, store): def test_golang_hook_still_works_when_gobin_is_set(tempdir_factory, store): gobin_dir = tempdir_factory.get() with envcontext([('GOBIN', gobin_dir)]): - _test_hook_repo( - tempdir_factory, store, 'golang_hooks_repo', - 'golang-hook', [], b'hello world\n', - ) - assert os.listdir(gobin_dir) == [], "hook must not be installed in $GOBIN" + test_golang_hook(tempdir_factory, store) + assert os.listdir(gobin_dir) == [] def test_rust_hook(tempdir_factory, store): From 1f3c6ce035469cdf38879b54f6109eb6e06f5d85 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 29 Jan 2019 22:09:47 -0800 Subject: [PATCH 339/544] Add W504 to ignored autopep8 rules Committed via https://github.com/asottile/all-repos --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index aaeadc28e..f63c3ce5d 100644 --- a/tox.ini +++ b/tox.ini @@ -17,7 +17,7 @@ envdir = venv-{[tox]project} commands = [pep8] -ignore = E265,E501 +ignore = E265,E501,W504 [pytest] env = From 29460606b2b749774db0ec3cfa384198a46e1a7b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 30 Jan 2019 00:39:01 -0800 Subject: [PATCH 340/544] Migrate to official pycqa/flake8 hooks repo Committed via https://github.com/asottile/all-repos --- .pre-commit-config.yaml | 13 ++++++++----- pre_commit/xargs.py | 4 ++-- tests/conftest.py | 6 +++--- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9ffdbe942..55e2d331b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.0.0 + rev: v2.1.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -10,21 +10,24 @@ repos: - id: debug-statements - id: name-tests-test - id: requirements-txt-fixer +- repo: https://gitlab.com/pycqa/flake8 + rev: 3.7.1 + hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.4 + rev: v1.4.3 hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v1.11.2 + rev: v1.14.2 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v1.11.0 + rev: v1.11.1 hooks: - id: pyupgrade - repo: https://github.com/asottile/reorder_python_imports - rev: v1.3.0 + rev: v1.3.5 hooks: - id: reorder-python-imports language_version: python3 diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index e2686f0f9..bd9205b7b 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -58,8 +58,8 @@ def partition(cmd, varargs, target_concurrency, _max_length=None): arg_length = _command_length(arg) + 1 if ( - total_length + arg_length <= _max_length - and len(ret_cmd) < max_args + total_length + arg_length <= _max_length and + len(ret_cmd) < max_args ): ret_cmd.append(arg) total_length += arg_length diff --git a/tests/conftest.py b/tests/conftest.py index c7d815620..baaa64c96 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -29,9 +29,9 @@ def no_warnings(recwarn): message = str(warning.message) # ImportWarning: Not importing directory '...' missing __init__(.py) if not ( - isinstance(warning.message, ImportWarning) - and message.startswith('Not importing directory ') - and ' missing __init__' in message + isinstance(warning.message, ImportWarning) and + message.startswith('Not importing directory ') and + ' missing __init__' in message ): warnings.append('{}:{} {}'.format( warning.filename, From 7b491c7110a2e2234ca19ff8b8d66f7efb1422fe Mon Sep 17 00:00:00 2001 From: Jesse Bona <37656694+jessebona@users.noreply.github.com> Date: Fri, 1 Feb 2019 19:15:59 +1100 Subject: [PATCH 341/544] Update migrate_config.py Added if statement to prevent looping through header lines if configuration file is empty --- pre_commit/commands/migrate_config.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index 3f73bb83e..47bb7695b 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -21,8 +21,10 @@ def _migrate_map(contents): # Find the first non-header line lines = contents.splitlines(True) i = 0 - while _is_header_line(lines[i]): - i += 1 + # Only loop on non empty configuration file + if i < len(lines): + while _is_header_line(lines[i]): + i += 1 header = ''.join(lines[:i]) rest = ''.join(lines[i:]) From f2be2ead352cab90718d73638f11aa8e4b070ca9 Mon Sep 17 00:00:00 2001 From: Jesse Bona <37656694+jessebona@users.noreply.github.com> Date: Sat, 2 Feb 2019 10:34:53 +1100 Subject: [PATCH 342/544] Update migrate_config.py Corrected loop condition to not run if configuration file only contains new lines. --- pre_commit/commands/migrate_config.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index 47bb7695b..bac423193 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -22,9 +22,8 @@ def _migrate_map(contents): lines = contents.splitlines(True) i = 0 # Only loop on non empty configuration file - if i < len(lines): - while _is_header_line(lines[i]): - i += 1 + while i < len(lines) and _is_header_line(lines[i]): + i += 1 header = ''.join(lines[:i]) rest = ''.join(lines[i:]) From 8a7142d7632372fe5493aa8bead9723462a9d86b Mon Sep 17 00:00:00 2001 From: Jesse Bona <37656694+jessebona@users.noreply.github.com> Date: Sat, 2 Feb 2019 10:38:04 +1100 Subject: [PATCH 343/544] Added test for blank configuration file --- tests/commands/migrate_config_test.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index 8f9153fdc..e07f721f3 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -147,3 +147,12 @@ def test_migrate_config_sha_to_rev(tmpdir): ' rev: v1.2.0\n' ' hooks: []\n' ) + +@pytest.mark.parametrize('contents', ('', '\n')) +def test_empty_configuration_file_user_error(tmpdir, contents): + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write(contents) + with tmpdir.as_cwd(): + assert not migrate_config(C.CONFIG_FILE) + # even though the config is invalid, this should be a noop + assert cfg.read() == contents From e2ee95d9b2f1bda70b1573bd9daa3c936d18dd49 Mon Sep 17 00:00:00 2001 From: Jesse Bona <37656694+jessebona@users.noreply.github.com> Date: Sat, 2 Feb 2019 11:32:09 +1100 Subject: [PATCH 344/544] Update migrate_config_test.py Added second blank line between test_migrate_config_sha_to_rev and test_empty_configuration_file_user_error --- tests/commands/migrate_config_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index e07f721f3..945d8b4ae 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -148,6 +148,7 @@ def test_migrate_config_sha_to_rev(tmpdir): ' hooks: []\n' ) + @pytest.mark.parametrize('contents', ('', '\n')) def test_empty_configuration_file_user_error(tmpdir, contents): cfg = tmpdir.join(C.CONFIG_FILE) From 1a3d296d8750deffe9688885380e12b175409d7b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 1 Feb 2019 16:47:08 -0800 Subject: [PATCH 345/544] Trailing whitespace too Github editor is a fickle beast --- tests/commands/migrate_config_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index 945d8b4ae..c58b9f74b 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -148,7 +148,7 @@ def test_migrate_config_sha_to_rev(tmpdir): ' hooks: []\n' ) - + @pytest.mark.parametrize('contents', ('', '\n')) def test_empty_configuration_file_user_error(tmpdir, contents): cfg = tmpdir.join(C.CONFIG_FILE) From 728349bc4b6e8765badb616d939979011799a87b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 3 Feb 2019 14:01:28 -0800 Subject: [PATCH 346/544] Require new virtualenv I'd like to start using metadata-based setup (`setup.cfg`) and this is the minimum virtualenv version needed to build those. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 240a7e3e0..f125430d7 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ 'pyyaml', 'six', 'toml', - 'virtualenv', + 'virtualenv>=15.2', ], extras_require={ ':python_version<"3.2"': ['futures'], From 2fa0fabb05e147417f0cd9c619f94547874bda46 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 4 Feb 2019 08:43:31 -0800 Subject: [PATCH 347/544] v1.14.3 --- CHANGELOG.md | 15 +++++++++++++++ setup.py | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13b1dd915..bdae2c4a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +1.14.3 +====== + +### Fixes +- Improve performance of filename classification by 45% - 55%. + - #921 PR by @asottile. +- Fix installing `go` hooks while `GOBIN` environment variable is set. + - #924 PR by @ashanbrown. +- Fix crash while running `pre-commit migrate-config` / `pre-commit autoupdate` + with an empty configuration file. + - #929 issue by @ardakuyumcu. + - #933 PR by @jessebona. +- Require a newer virtualenv to fix metadata-based setup.cfg installs. + - #936 PR by @asottile. + 1.14.2 ====== diff --git a/setup.py b/setup.py index f125430d7..250b0d3e7 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.14.2', + version='1.14.3', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From db04d612e07f527c09d52afb8f2dc4971adfc70e Mon Sep 17 00:00:00 2001 From: Benjamin Bariteau Date: Fri, 15 Feb 2019 14:37:53 -0800 Subject: [PATCH 348/544] pass GIT_SSH_COMMAND to git commands, refs #947 --- pre_commit/git.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index f0b504043..4849d7c64 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -29,7 +29,7 @@ def no_git_env(): # GIT_INDEX_FILE: Causes 'error invalid object ...' during commit return { k: v for k, v in os.environ.items() - if not k.startswith('GIT_') or k in {'GIT_SSH'} + if not k.startswith('GIT_') or k in {'GIT_SSH', 'GIT_SSH_COMMAND'} } From 9cde231665f5389adca9e39ff5fe8ddedd5c65fe Mon Sep 17 00:00:00 2001 From: Min RK Date: Thu, 14 Feb 2019 15:45:18 +0100 Subject: [PATCH 349/544] respect GIT_EXEC_PATH env This env may be required for git to work, unsetting it can cause clone to fail occurs with bundled git, e.g. Fork git client --- pre_commit/git.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 4849d7c64..06c847f36 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -29,7 +29,8 @@ def no_git_env(): # GIT_INDEX_FILE: Causes 'error invalid object ...' during commit return { k: v for k, v in os.environ.items() - if not k.startswith('GIT_') or k in {'GIT_SSH', 'GIT_SSH_COMMAND'} + if not k.startswith('GIT_') or + k in {'GIT_EXEC_PATH', 'GIT_SSH', 'GIT_SSH_COMMAND'} } From 136834038d915d46fb93ea88ec76251158732686 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 17 Feb 2019 10:13:49 -0800 Subject: [PATCH 350/544] Use npm install git+file:// instead of npm install . --- pre_commit/languages/node.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index b313bf5b5..6fd7e53cc 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -62,10 +62,11 @@ def install_environment(prefix, version, additional_dependencies): cmd.extend(['-n', version]) cmd_output(*cmd) + dep = 'git+file://{}'.format(prefix.prefix_dir) with in_env(prefix, version): helpers.run_setup_cmd( prefix, - ('npm', 'install', '-g', '.') + additional_dependencies, + ('npm', 'install', '-g', dep) + additional_dependencies, ) From 6088b1f9953c6322569d21805238bd9b74bc0439 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 17 Feb 2019 12:17:46 -0800 Subject: [PATCH 351/544] 3 slashes works around an npm bug https://npm.community/t/npm-install-g-git-file-c-path-to-repository-does-not-work-on-windows/5453 --- pre_commit/languages/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 6fd7e53cc..e7962cce1 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -62,7 +62,7 @@ def install_environment(prefix, version, additional_dependencies): cmd.extend(['-n', version]) cmd_output(*cmd) - dep = 'git+file://{}'.format(prefix.prefix_dir) + dep = 'git+file:///{}'.format(prefix.prefix_dir) with in_env(prefix, version): helpers.run_setup_cmd( prefix, From aa4bc9d241d805d67efa29f040b29fe3baba5523 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 18 Feb 2019 09:13:54 -0800 Subject: [PATCH 352/544] v1.14.4 --- CHANGELOG.md | 14 ++++++++++++++ setup.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bdae2c4a6..129447a40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +1.14.4 +====== + +### Fixes +- Don't filter `GIT_SSH_COMMAND` env variable from `git` commands + - #947 issue by @firba1. + - #948 PR by @firba1. +- Install npm packages as if they were installed from `git` + - #943 issue by @ssbarnea. + - #949 PR by @asottile. +- Don't filter `GIT_EXEC_PREFIX` env variable from `git` commands + - #664 issue by @revolter. + - #944 PR by @minrk. + 1.14.3 ====== diff --git a/setup.py b/setup.py index 250b0d3e7..6bb15bd9a 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.14.3', + version='1.14.4', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From f9cfaef5aa94afe1d74599a068d80e83fb11e8a6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 27 Feb 2019 22:12:03 -0800 Subject: [PATCH 353/544] Migrate setup.py to setup.cfg declarative metadata Committed via https://github.com/asottile/all-repos --- setup.cfg | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 60 +------------------------------------------------------ 2 files changed, 57 insertions(+), 59 deletions(-) diff --git a/setup.cfg b/setup.cfg index 2be683657..178e492eb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,58 @@ +[metadata] +name = pre_commit +version = 1.14.4 +description = A framework for managing and maintaining multi-language pre-commit hooks. +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/pre-commit/pre-commit +author = Anthony Sottile +author_email = asottile@umich.edu +license = MIT +license_file = LICENSE +classifiers = + License :: OSI Approved :: MIT License + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: Implementation :: CPython + Programming Language :: Python :: Implementation :: PyPy + +[options] +packages = find: +install_requires = + aspy.yaml + cfgv>=1.4.0 + identify>=1.0.0 + importlib-metadata + nodeenv>=0.11.1 + pyyaml + six + toml + virtualenv>=15.2 + futures; python_version<"3.2" + importlib-resources; python_version<"3.7" +python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* + +[options.entry_points] +console_scripts = + pre-commit = pre_commit.main:main + pre-commit-validate-config = pre_commit.clientlib:validate_config_main + pre-commit-validate-manifest = pre_commit.clientlib:validate_manifest_main + +[options.package_data] +pre_commit.resources = + *.tar.gz + empty_template_* + hook-tmpl + +[options.packages.find] +exclude = + tests* + testing* + [bdist_wheel] universal = True diff --git a/setup.py b/setup.py index 6bb15bd9a..8bf1ba938 100644 --- a/setup.py +++ b/setup.py @@ -1,60 +1,2 @@ -from setuptools import find_packages from setuptools import setup - -with open('README.md') as f: - long_description = f.read() - -setup( - name='pre_commit', - description=( - 'A framework for managing and maintaining multi-language pre-commit ' - 'hooks.' - ), - long_description=long_description, - long_description_content_type='text/markdown', - url='https://github.com/pre-commit/pre-commit', - version='1.14.4', - author='Anthony Sottile', - author_email='asottile@umich.edu', - classifiers=[ - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', - ], - packages=find_packages(exclude=('tests*', 'testing*')), - package_data={ - 'pre_commit.resources': [ - '*.tar.gz', - 'empty_template_*', - 'hook-tmpl', - ], - }, - install_requires=[ - 'aspy.yaml', - 'cfgv>=1.4.0', - 'identify>=1.0.0', - # if this makes it into python3.8 move to extras_require - 'importlib-metadata', - 'nodeenv>=0.11.1', - 'pyyaml', - 'six', - 'toml', - 'virtualenv>=15.2', - ], - extras_require={ - ':python_version<"3.2"': ['futures'], - ':python_version<"3.7"': ['importlib-resources'], - }, - entry_points={ - 'console_scripts': [ - 'pre-commit = pre_commit.main:main', - 'pre-commit-validate-config = pre_commit.clientlib:validate_config_main', # noqa - 'pre-commit-validate-manifest = pre_commit.clientlib:validate_manifest_main', # noqa - ], - }, -) +setup() From e74253d2def66bc9927f5e8122867b35b315215a Mon Sep 17 00:00:00 2001 From: DanielChabrowski Date: Sun, 3 Mar 2019 01:35:53 +0100 Subject: [PATCH 354/544] Allow shallow cloning --- pre_commit/store.py | 62 ++++++++++++++++++++++++++++++++++----------- testing/util.py | 5 ++++ tests/store_test.py | 38 +++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 15 deletions(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index 8301ecad8..9fa481272 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -10,6 +10,7 @@ import pre_commit.constants as C from pre_commit import file_lock from pre_commit import git +from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import resource_text @@ -121,10 +122,7 @@ def _get_result(): return result logger.info('Initializing environment for {}.'.format(repo)) - - directory = tempfile.mkdtemp(prefix='repo', dir=self.directory) - with clean_path_on_failure(directory): - make_strategy(directory) + directory = make_strategy() # Update our db with the created repo with self.connect() as db: @@ -134,19 +132,50 @@ def _get_result(): ) return directory - def clone(self, repo, ref, deps=()): - """Clone the given url and checkout the specific ref.""" - def clone_strategy(directory): - env = git.no_git_env() + def _perform_safe_clone(self, clone_strategy): + directory = tempfile.mkdtemp(prefix='repo', dir=self.directory) + with clean_path_on_failure(directory): + clone_strategy(directory) + return directory - cmd = ('git', 'clone', '--no-checkout', repo, directory) - cmd_output(*cmd, env=env) + def _complete_clone(self, repo, ref, directory): + """Perform a complete clone of a repository and its submodules """ + env = git.no_git_env() - def _git_cmd(*args): - return cmd_output('git', *args, cwd=directory, env=env) + cmd = ('git', 'clone', '--no-checkout', repo, directory) + cmd_output(*cmd, env=env) + + def _git_cmd(*args): + return cmd_output('git', *args, cwd=directory, env=env) + + _git_cmd('reset', ref, '--hard') + _git_cmd('submodule', 'update', '--init', '--recursive') - _git_cmd('reset', ref, '--hard') - _git_cmd('submodule', 'update', '--init', '--recursive') + def _shallow_clone(self, repo, ref, directory): + """Perform a shallow clone of a repository and its submodules """ + env = git.no_git_env() + + def _git_cmd(*args): + return cmd_output('git', *args, cwd=directory, env=env) + + _git_cmd('init', '.') + _git_cmd('remote', 'add', 'origin', repo) + _git_cmd('fetch', 'origin', ref, '--depth=1') + _git_cmd('checkout', ref) + _git_cmd('submodule', 'update', '--init', '--recursive', '--depth=1') + + def clone(self, repo, ref, deps=()): + """Clone the given url and checkout the specific ref.""" + + def clone_strategy(): + try: + def shallow_clone(directory): + self._shallow_clone(repo, ref, directory) + return self._perform_safe_clone(shallow_clone) + except CalledProcessError: + def complete_clone(directory): + self._complete_clone(repo, ref, directory) + return self._perform_safe_clone(complete_clone) return self._new_repo(repo, ref, deps, clone_strategy) @@ -173,8 +202,11 @@ def _git_cmd(*args): _git_cmd('add', '.') git.commit(repo=directory) + def make_strategy(): + return self._perform_safe_clone(make_local_strategy) + return self._new_repo( - 'local', C.LOCAL_REPO_VERSION, deps, make_local_strategy, + 'local', C.LOCAL_REPO_VERSION, deps, make_strategy, ) def _create_config_table_if_not_exists(self, db): diff --git a/testing/util.py b/testing/util.py index 156967302..f4dda0a97 100644 --- a/testing/util.py +++ b/testing/util.py @@ -142,3 +142,8 @@ def git_commit(*args, **kwargs): if msg is not None: # allow skipping `-m` with `msg=None` cmd += ('-m', msg) return fn(*cmd, **kwargs) + + +def git_ref_count(repo): + _, out, _ = cmd_output('git', 'rev-list', '--all', '--count', cwd=repo) + return int(out.split()[0]) diff --git a/tests/store_test.py b/tests/store_test.py index 238343fda..c3de68914 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -12,9 +12,11 @@ from pre_commit import git from pre_commit.store import _get_default_directory from pre_commit.store import Store +from pre_commit.util import CalledProcessError from testing.fixtures import git_dir from testing.util import cwd from testing.util import git_commit +from testing.util import git_ref_count def test_our_session_fixture_works(): @@ -81,6 +83,7 @@ def test_clone(store, tempdir_factory, log_info_mock): assert dirname.startswith('repo') # Should be checked out to the rev we specified assert git.head_rev(ret) == rev + assert git_ref_count(ret) == 1 # Assert there's an entry in the sqlite db for this assert store.select_all_repos() == [(path, rev, ret)] @@ -111,6 +114,41 @@ def test_clone_when_repo_already_exists(store): assert store.clone('fake_repo', 'fake_ref') == 'fake_path' +def test_clone_shallow_failure_fallback_to_complete( + store, tempdir_factory, + log_info_mock, +): + path = git_dir(tempdir_factory) + with cwd(path): + git_commit() + rev = git.head_rev(path) + git_commit() + + # Force shallow clone failure + def fake_shallow_clone(self, *args, **kwargs): + raise CalledProcessError(None, None, None) + store._shallow_clone = fake_shallow_clone + + ret = store.clone(path, rev) + + # Should have printed some stuff + assert log_info_mock.call_args_list[0][0][0].startswith( + 'Initializing environment for ', + ) + + # Should return a directory inside of the store + assert os.path.exists(ret) + assert ret.startswith(store.directory) + # Directory should start with `repo` + _, dirname = os.path.split(ret) + assert dirname.startswith('repo') + # Should be checked out to the rev we specified + assert git.head_rev(ret) == rev + + # Assert there's an entry in the sqlite db for this + assert store.select_all_repos() == [(path, rev, ret)] + + def test_create_when_directory_exists_but_not_db(store): # In versions <= 0.3.5, there was no sqlite db causing a need for # backward compatibility From 917586a0e0c59c155dae0342dd25c03388035881 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 2 Mar 2019 19:00:59 -0800 Subject: [PATCH 355/544] Don't require git for clean, gc, sample-config --- pre_commit/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index be0fa7f03..a935cf1cf 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -255,7 +255,8 @@ def main(argv=None): parser.parse_args(['--help']) with error_handler(), logging_handler(args.color): - _adjust_args_and_chdir(args) + if args.command not in {'clean', 'gc', 'sample-config'}: + _adjust_args_and_chdir(args) git.check_for_cygwin_mismatch() From b920f3cc6bacc0fafa0aef3edf817ea0f88bc46b Mon Sep 17 00:00:00 2001 From: DanielChabrowski Date: Sat, 9 Mar 2019 22:59:56 +0100 Subject: [PATCH 356/544] Reuse the directory for cloning --- pre_commit/store.py | 67 ++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 38 deletions(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index 9fa481272..943c5a8d1 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -122,7 +122,10 @@ def _get_result(): return result logger.info('Initializing environment for {}.'.format(repo)) - directory = make_strategy() + + directory = tempfile.mkdtemp(prefix='repo', dir=self.directory) + with clean_path_on_failure(directory): + make_strategy(directory) # Update our db with the created repo with self.connect() as db: @@ -132,50 +135,41 @@ def _get_result(): ) return directory - def _perform_safe_clone(self, clone_strategy): - directory = tempfile.mkdtemp(prefix='repo', dir=self.directory) - with clean_path_on_failure(directory): - clone_strategy(directory) - return directory - - def _complete_clone(self, repo, ref, directory): + def _complete_clone(self, ref, git_cmd): """Perform a complete clone of a repository and its submodules """ - env = git.no_git_env() - - cmd = ('git', 'clone', '--no-checkout', repo, directory) - cmd_output(*cmd, env=env) - def _git_cmd(*args): - return cmd_output('git', *args, cwd=directory, env=env) + git_cmd('fetch', 'origin') + git_cmd('checkout', ref) + git_cmd('submodule', 'update', '--init', '--recursive') - _git_cmd('reset', ref, '--hard') - _git_cmd('submodule', 'update', '--init', '--recursive') - - def _shallow_clone(self, repo, ref, directory): + def _shallow_clone(self, ref, protocol_version, git_cmd): """Perform a shallow clone of a repository and its submodules """ - env = git.no_git_env() - - def _git_cmd(*args): - return cmd_output('git', *args, cwd=directory, env=env) - _git_cmd('init', '.') - _git_cmd('remote', 'add', 'origin', repo) - _git_cmd('fetch', 'origin', ref, '--depth=1') - _git_cmd('checkout', ref) - _git_cmd('submodule', 'update', '--init', '--recursive', '--depth=1') + git_config = 'protocol.version={}'.format(protocol_version) + git_cmd('-c', git_config, 'fetch', 'origin', ref, '--depth=1') + git_cmd('checkout', ref) + git_cmd('-c', git_config, 'submodule', 'update', '--init', + '--recursive', '--depth=1') def clone(self, repo, ref, deps=()): """Clone the given url and checkout the specific ref.""" - def clone_strategy(): + def clone_strategy(directory): + env = git.no_git_env() + + def _git_cmd(*args): + cmd_output('git', *args, cwd=directory, env=env) + + _git_cmd('init', '.') + _git_cmd('remote', 'add', 'origin', repo) + try: - def shallow_clone(directory): - self._shallow_clone(repo, ref, directory) - return self._perform_safe_clone(shallow_clone) + self._shallow_clone(ref, 2, _git_cmd) except CalledProcessError: - def complete_clone(directory): - self._complete_clone(repo, ref, directory) - return self._perform_safe_clone(complete_clone) + try: + self._shallow_clone(ref, 1, _git_cmd) + except CalledProcessError: + self._complete_clone(ref, _git_cmd) return self._new_repo(repo, ref, deps, clone_strategy) @@ -202,11 +196,8 @@ def _git_cmd(*args): _git_cmd('add', '.') git.commit(repo=directory) - def make_strategy(): - return self._perform_safe_clone(make_local_strategy) - return self._new_repo( - 'local', C.LOCAL_REPO_VERSION, deps, make_strategy, + 'local', C.LOCAL_REPO_VERSION, deps, make_local_strategy, ) def _create_config_table_if_not_exists(self, db): From 960bcc96141c2440923145603e61a3fc11d23e0e Mon Sep 17 00:00:00 2001 From: DanielChabrowski Date: Sat, 9 Mar 2019 23:56:37 +0100 Subject: [PATCH 357/544] Fix relative path repos --- pre_commit/store.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pre_commit/store.py b/pre_commit/store.py index 943c5a8d1..75fbceb0f 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -154,6 +154,9 @@ def _shallow_clone(self, ref, protocol_version, git_cmd): def clone(self, repo, ref, deps=()): """Clone the given url and checkout the specific ref.""" + if os.path.isdir(repo): + repo = os.path.abspath(repo) + def clone_strategy(directory): env = git.no_git_env() From 985f09ff887d4d94e8473a9392dc10318a192e30 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 9 Mar 2019 14:36:24 -0800 Subject: [PATCH 358/544] Compute the maximum command length more accurately --- pre_commit/xargs.py | 29 ++++++++++++++++++++++------- tests/xargs_test.py | 31 +++++++++++++++++++++++-------- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index bd9205b7b..a382759c4 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -5,6 +5,7 @@ import concurrent.futures import contextlib import math +import os import sys import six @@ -13,10 +14,24 @@ from pre_commit.util import cmd_output -# TODO: properly compute max_length value -def _get_platform_max_length(): - # posix minimum - return 4 * 1024 +def _environ_size(_env=None): + environ = _env if _env is not None else getattr(os, 'environb', os.environ) + size = 8 * len(environ) # number of pointers in `envp` + for k, v in environ.items(): + size += len(k) + len(v) + 2 # c strings in `envp` + return size + + +def _get_platform_max_length(): # pragma: no cover (platform specific) + if os.name == 'posix': + maximum = os.sysconf(str('SC_ARG_MAX')) - 2048 - _environ_size() + maximum = min(maximum, 2 ** 17) + return maximum + elif os.name == 'nt': + return 2 ** 15 - 2048 # UNICODE_STRING max - headroom + else: + # posix minimum + return 2 ** 12 def _command_length(*cmd): @@ -52,7 +67,7 @@ def partition(cmd, varargs, target_concurrency, _max_length=None): # Reversed so arguments are in order varargs = list(reversed(varargs)) - total_length = _command_length(*cmd) + total_length = _command_length(*cmd) + 1 while varargs: arg = varargs.pop() @@ -69,7 +84,7 @@ def partition(cmd, varargs, target_concurrency, _max_length=None): # We've exceeded the length, yield a command ret.append(cmd + tuple(ret_cmd)) ret_cmd = [] - total_length = _command_length(*cmd) + total_length = _command_length(*cmd) + 1 varargs.append(arg) ret.append(cmd + tuple(ret_cmd)) @@ -99,7 +114,7 @@ def xargs(cmd, varargs, **kwargs): stderr = b'' try: - parse_shebang.normexe(cmd[0]) + cmd = parse_shebang.normalize_cmd(cmd) except parse_shebang.ExecutableNotFoundError as e: return e.to_output() diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 0e91f9be8..a6cffd727 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -10,9 +10,24 @@ import pytest import six +from pre_commit import parse_shebang from pre_commit import xargs +@pytest.mark.parametrize( + ('env', 'expected'), + ( + ({}, 0), + ({b'x': b'1'}, 12), + ({b'x': b'12'}, 13), + ({b'x': b'1', b'y': b'2'}, 24), + ), +) +def test_environ_size(env, expected): + # normalize integer sizing + assert xargs._environ_size(_env=env) == expected + + @pytest.fixture def win32_py2_mock(): with mock.patch.object(sys, 'getfilesystemencoding', return_value='utf-8'): @@ -56,7 +71,7 @@ def test_partition_limits(): '.' * 6, ), 1, - _max_length=20, + _max_length=21, ) assert ret == ( ('ninechars', '.' * 5, '.' * 4), @@ -70,21 +85,21 @@ def test_partition_limit_win32_py3(win32_py3_mock): cmd = ('ninechars',) # counted as half because of utf-16 encode varargs = ('😑' * 5,) - ret = xargs.partition(cmd, varargs, 1, _max_length=20) + ret = xargs.partition(cmd, varargs, 1, _max_length=21) assert ret == (cmd + varargs,) def test_partition_limit_win32_py2(win32_py2_mock): cmd = ('ninechars',) varargs = ('😑' * 5,) # 4 bytes * 5 - ret = xargs.partition(cmd, varargs, 1, _max_length=30) + ret = xargs.partition(cmd, varargs, 1, _max_length=31) assert ret == (cmd + varargs,) def test_partition_limit_linux(linux_mock): cmd = ('ninechars',) varargs = ('😑' * 5,) - ret = xargs.partition(cmd, varargs, 1, _max_length=30) + ret = xargs.partition(cmd, varargs, 1, _max_length=31) assert ret == (cmd + varargs,) @@ -134,9 +149,9 @@ def test_xargs_smoke(): assert err == b'' -exit_cmd = ('bash', '-c', 'exit $1', '--') +exit_cmd = parse_shebang.normalize_cmd(('bash', '-c', 'exit $1', '--')) # Abuse max_length to control the exit code -max_length = len(' '.join(exit_cmd)) + 2 +max_length = len(' '.join(exit_cmd)) + 3 def test_xargs_negate(): @@ -165,14 +180,14 @@ def test_xargs_retcode_normal(): def test_xargs_concurrency(): - bash_cmd = ('bash', '-c') + bash_cmd = parse_shebang.normalize_cmd(('bash', '-c')) print_pid = ('sleep 0.5 && echo $$',) start = time.time() ret, stdout, _ = xargs.xargs( bash_cmd, print_pid * 5, target_concurrency=5, - _max_length=len(' '.join(bash_cmd + print_pid)), + _max_length=len(' '.join(bash_cmd + print_pid)) + 1, ) elapsed = time.time() - start assert ret == 0 From 7a763a985122082ed55eda040f432ef9487179d4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 10 Mar 2019 11:27:25 -0700 Subject: [PATCH 359/544] Improve testsuite speed on windows by ~23 seconds --- tests/commands/run_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index e37eca646..f6efe244c 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -499,7 +499,7 @@ def test_stdout_write_bug_py26(repo_with_failing_hook, store, tempdir_factory): def test_lots_of_files(store, tempdir_factory): # windows xargs seems to have a bug, here's a regression test for # our workaround - git_path = make_consuming_repo(tempdir_factory, 'python_hooks_repo') + git_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(git_path): # Override files so we run against them with modify_config() as config: From 3cb35e8679b2e8c09398953b19bd063ceabfc665 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 14 Mar 2019 18:20:30 -0700 Subject: [PATCH 360/544] Revert "Merge pull request #949 from asottile/npm_install_git" This reverts commit a4c1a701bcd70a4a27b4bd0d9832a447c782daa9, reversing changes made to 889124b5ca31d51f8849a8aaca70b3cfaa742de5. --- pre_commit/languages/node.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index e7962cce1..b313bf5b5 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -62,11 +62,10 @@ def install_environment(prefix, version, additional_dependencies): cmd.extend(['-n', version]) cmd_output(*cmd) - dep = 'git+file:///{}'.format(prefix.prefix_dir) with in_env(prefix, version): helpers.run_setup_cmd( prefix, - ('npm', 'install', '-g', dep) + additional_dependencies, + ('npm', 'install', '-g', '.') + additional_dependencies, ) From d71a75fea2ebd3416353f2e2bf9d9c6139501ad3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 14 Mar 2019 18:31:57 -0700 Subject: [PATCH 361/544] Run `npm install` before `npm install -g` --- pre_commit/languages/node.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index b313bf5b5..aac1c591d 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -63,6 +63,9 @@ def install_environment(prefix, version, additional_dependencies): cmd_output(*cmd) with in_env(prefix, version): + # https://npm.community/t/npm-install-g-git-vs-git-clone-cd-npm-install-g/5449 + # install as if we installed from git + helpers.run_setup_cmd(prefix, ('npm', 'install')) helpers.run_setup_cmd( prefix, ('npm', 'install', '-g', '.') + additional_dependencies, From ec2e15f086aab3510a1509650de9191819d551b1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 14 Mar 2019 18:32:27 -0700 Subject: [PATCH 362/544] pre-commit run --all-files --- pre_commit/store.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index 75fbceb0f..7a85d03ed 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -148,8 +148,10 @@ def _shallow_clone(self, ref, protocol_version, git_cmd): git_config = 'protocol.version={}'.format(protocol_version) git_cmd('-c', git_config, 'fetch', 'origin', ref, '--depth=1') git_cmd('checkout', ref) - git_cmd('-c', git_config, 'submodule', 'update', '--init', - '--recursive', '--depth=1') + git_cmd( + '-c', git_config, 'submodule', 'update', '--init', + '--recursive', '--depth=1', + ) def clone(self, repo, ref, deps=()): """Clone the given url and checkout the specific ref.""" From e748da2abe1b5916847af3380dfb9b633a4b171c Mon Sep 17 00:00:00 2001 From: DanielChabrowski Date: Fri, 15 Mar 2019 23:25:04 +0100 Subject: [PATCH 363/544] Remove clone depth check --- testing/util.py | 5 ----- tests/store_test.py | 2 -- 2 files changed, 7 deletions(-) diff --git a/testing/util.py b/testing/util.py index f4dda0a97..156967302 100644 --- a/testing/util.py +++ b/testing/util.py @@ -142,8 +142,3 @@ def git_commit(*args, **kwargs): if msg is not None: # allow skipping `-m` with `msg=None` cmd += ('-m', msg) return fn(*cmd, **kwargs) - - -def git_ref_count(repo): - _, out, _ = cmd_output('git', 'rev-list', '--all', '--count', cwd=repo) - return int(out.split()[0]) diff --git a/tests/store_test.py b/tests/store_test.py index c3de68914..662175880 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -16,7 +16,6 @@ from testing.fixtures import git_dir from testing.util import cwd from testing.util import git_commit -from testing.util import git_ref_count def test_our_session_fixture_works(): @@ -83,7 +82,6 @@ def test_clone(store, tempdir_factory, log_info_mock): assert dirname.startswith('repo') # Should be checked out to the rev we specified assert git.head_rev(ret) == rev - assert git_ref_count(ret) == 1 # Assert there's an entry in the sqlite db for this assert store.select_all_repos() == [(path, rev, ret)] From a170e60daac3ac5a39e334ab4d34c43c762e6f25 Mon Sep 17 00:00:00 2001 From: DanielChabrowski Date: Fri, 15 Mar 2019 23:46:35 +0100 Subject: [PATCH 364/544] Remove protocol.version 1 shallow cloning --- pre_commit/store.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index 7a85d03ed..09116861c 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -142,10 +142,10 @@ def _complete_clone(self, ref, git_cmd): git_cmd('checkout', ref) git_cmd('submodule', 'update', '--init', '--recursive') - def _shallow_clone(self, ref, protocol_version, git_cmd): + def _shallow_clone(self, ref, git_cmd): """Perform a shallow clone of a repository and its submodules """ - git_config = 'protocol.version={}'.format(protocol_version) + git_config = 'protocol.version=2' git_cmd('-c', git_config, 'fetch', 'origin', ref, '--depth=1') git_cmd('checkout', ref) git_cmd( @@ -169,12 +169,9 @@ def _git_cmd(*args): _git_cmd('remote', 'add', 'origin', repo) try: - self._shallow_clone(ref, 2, _git_cmd) + self._shallow_clone(ref, _git_cmd) except CalledProcessError: - try: - self._shallow_clone(ref, 1, _git_cmd) - except CalledProcessError: - self._complete_clone(ref, _git_cmd) + self._complete_clone(ref, _git_cmd) return self._new_repo(repo, ref, deps, clone_strategy) From ab1df034182b3a699dcf05ec5c8f7a8eba7c8fae Mon Sep 17 00:00:00 2001 From: DanielChabrowski Date: Sat, 16 Mar 2019 00:16:39 +0100 Subject: [PATCH 365/544] Ignore shallow clone coverage on appveyor Appveyor uses old version of git so shallow clone always fails and lines 150-151 are not executed. --- pre_commit/store.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index 09116861c..93a9cab32 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -142,7 +142,7 @@ def _complete_clone(self, ref, git_cmd): git_cmd('checkout', ref) git_cmd('submodule', 'update', '--init', '--recursive') - def _shallow_clone(self, ref, git_cmd): + def _shallow_clone(self, ref, git_cmd): # pragma: windows no cover """Perform a shallow clone of a repository and its submodules """ git_config = 'protocol.version=2' From f673f8bb55697b64a12eae1a2b4df49286e6c2e6 Mon Sep 17 00:00:00 2001 From: Brett Randall Date: Mon, 18 Mar 2019 09:45:56 +1100 Subject: [PATCH 366/544] Added double-quote-string-fixer pre-commit hook. Signed-off-by: Brett Randall --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 55e2d331b..e7ecdf884 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,6 +10,7 @@ repos: - id: debug-statements - id: name-tests-test - id: requirements-txt-fixer + - id: double-quote-string-fixer - repo: https://gitlab.com/pycqa/flake8 rev: 3.7.1 hooks: From f5af95cc9d6eab1551366b04ec29df8dfcf39ec9 Mon Sep 17 00:00:00 2001 From: Brett Randall Date: Sun, 17 Mar 2019 22:48:14 +1100 Subject: [PATCH 367/544] Added test for git.no_git_env(). Signed-off-by: Brett Randall --- pre_commit/git.py | 5 +++-- tests/git_test.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 06c847f36..c24ca86e8 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -18,7 +18,7 @@ def zsplit(s): return [] -def no_git_env(): +def no_git_env(_env=None): # Too many bugs dealing with environment variables and GIT: # https://github.com/pre-commit/pre-commit/issues/300 # In git 2.6.3 (maybe others), git exports GIT_WORK_TREE while running @@ -27,8 +27,9 @@ def no_git_env(): # while running pre-commit hooks in submodules. # GIT_DIR: Causes git clone to clone wrong thing # GIT_INDEX_FILE: Causes 'error invalid object ...' during commit + _env = _env if _env is not None else os.environ return { - k: v for k, v in os.environ.items() + k: v for k, v in _env.items() if not k.startswith('GIT_') or k in {'GIT_EXEC_PATH', 'GIT_SSH', 'GIT_SSH_COMMAND'} } diff --git a/tests/git_test.py b/tests/git_test.py index 43f1c1569..299729dbc 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -173,3 +173,20 @@ def test_status_output_with_rename(in_git_dir): cmd_output('git', 'add', '--intent-to-add', 'c') assert git.intent_to_add_files() == ['c'] + + +def test_no_git_env(): + env = { + 'http_proxy': 'http://myproxy:80', + 'GIT_EXEC_PATH': '/some/git/exec/path', + 'GIT_SSH': '/usr/bin/ssh', + 'GIT_SSH_COMMAND': 'ssh -o', + 'GIT_DIR': '/none/shall/pass', + } + no_git_env = git.no_git_env(env) + assert no_git_env == { + 'http_proxy': 'http://myproxy:80', + 'GIT_EXEC_PATH': '/some/git/exec/path', + 'GIT_SSH': '/usr/bin/ssh', + 'GIT_SSH_COMMAND': 'ssh -o', + } From 7d7c9c0fde7b744950c23884db1316dc802cfd92 Mon Sep 17 00:00:00 2001 From: Brett Randall Date: Mon, 18 Mar 2019 10:24:46 +1100 Subject: [PATCH 368/544] Additional fixes prompted by double-quote-string-fixer. Signed-off-by: Brett Randall --- pre_commit/color_windows.py | 10 +++++----- .../meta_hooks/check_useless_excludes_test.py | 4 ++-- tests/repository_test.py | 4 ++-- tests/util_test.py | 20 +++++++++---------- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/pre_commit/color_windows.py b/pre_commit/color_windows.py index 4e193f967..9b8555e8d 100644 --- a/pre_commit/color_windows.py +++ b/pre_commit/color_windows.py @@ -20,18 +20,18 @@ def bool_errcheck(result, func, args): GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)( - ("GetStdHandle", windll.kernel32), ((1, "nStdHandle"),), + ('GetStdHandle', windll.kernel32), ((1, 'nStdHandle'),), ) GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))( - ("GetConsoleMode", windll.kernel32), - ((1, "hConsoleHandle"), (2, "lpMode")), + ('GetConsoleMode', windll.kernel32), + ((1, 'hConsoleHandle'), (2, 'lpMode')), ) GetConsoleMode.errcheck = bool_errcheck SetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, DWORD)( - ("SetConsoleMode", windll.kernel32), - ((1, "hConsoleHandle"), (1, "dwMode")), + ('SetConsoleMode', windll.kernel32), + ((1, 'hConsoleHandle'), (1, 'dwMode')), ) SetConsoleMode.errcheck = bool_errcheck diff --git a/tests/meta_hooks/check_useless_excludes_test.py b/tests/meta_hooks/check_useless_excludes_test.py index 4adaacd38..d261e8142 100644 --- a/tests/meta_hooks/check_useless_excludes_test.py +++ b/tests/meta_hooks/check_useless_excludes_test.py @@ -40,7 +40,7 @@ def test_useless_exclude_for_hook(capsys, in_git_dir): out = out.strip() expected = ( "The exclude pattern 'foo' for check-useless-excludes " - "does not match any files" + 'does not match any files' ) assert expected == out @@ -69,7 +69,7 @@ def test_useless_exclude_with_types_filter(capsys, in_git_dir): out = out.strip() expected = ( "The exclude pattern '.pre-commit-config.yaml' for " - "check-useless-excludes does not match any files" + 'check-useless-excludes does not match any files' ) assert expected == out diff --git a/tests/repository_test.py b/tests/repository_test.py index 5f03a455a..32915f1af 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -528,7 +528,7 @@ def test_local_golang_additional_dependencies(store): } ret = _get_hook(config, store, 'hello').run(()) assert ret[0] == 0 - assert _norm_out(ret[1]) == b"Hello, Go examples!\n" + assert _norm_out(ret[1]) == b'Hello, Go examples!\n' def test_local_rust_additional_dependencies(store): @@ -544,7 +544,7 @@ def test_local_rust_additional_dependencies(store): } ret = _get_hook(config, store, 'hello').run(()) assert ret[0] == 0 - assert _norm_out(ret[1]) == b"Hello World!\n" + assert _norm_out(ret[1]) == b'Hello World!\n' def test_fail_hooks(store): diff --git a/tests/util_test.py b/tests/util_test.py index 8178bb4bf..94c6ae630 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -17,12 +17,12 @@ def test_CalledProcessError_str(): ) assert str(error) == ( "Command: ['git', 'status']\n" - "Return code: 1\n" - "Expected return code: 0\n" - "Output: \n" - " stdout\n" - "Errors: \n" - " stderr\n" + 'Return code: 1\n' + 'Expected return code: 0\n' + 'Output: \n' + ' stdout\n' + 'Errors: \n' + ' stderr\n' ) @@ -32,10 +32,10 @@ def test_CalledProcessError_str_nooutput(): ) assert str(error) == ( "Command: ['git', 'status']\n" - "Return code: 1\n" - "Expected return code: 0\n" - "Output: (none)\n" - "Errors: (none)\n" + 'Return code: 1\n' + 'Expected return code: 0\n' + 'Output: (none)\n' + 'Errors: (none)\n' ) From 888787fb2de4cbf6772a98ca006a0e8d5b270d15 Mon Sep 17 00:00:00 2001 From: DanielChabrowski Date: Sun, 17 Mar 2019 22:09:38 +0100 Subject: [PATCH 369/544] Fix try-repo for staged untracked changes --- pre_commit/commands/try_repo.py | 6 ++++++ pre_commit/git.py | 3 ++- tests/commands/try_repo_test.py | 12 ++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index c9849ea4c..4bffd7544 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -32,9 +32,15 @@ def _repo_ref(tmpdir, repo, ref): shadow = os.path.join(tmpdir, 'shadow-repo') cmd_output('git', 'clone', repo, shadow) cmd_output('git', 'checkout', ref, '-b', '_pc_tmp', cwd=shadow) + idx = git.git_path('index', repo=shadow) objs = git.git_path('objects', repo=shadow) env = dict(os.environ, GIT_INDEX_FILE=idx, GIT_OBJECT_DIRECTORY=objs) + + staged_files = git.get_staged_files(cwd=repo) + if (len(staged_files) > 0): + cmd_output('git', 'add', *staged_files, cwd=repo, env=env) + cmd_output('git', 'add', '-u', cwd=repo, env=env) git.commit(repo=shadow) diff --git a/pre_commit/git.py b/pre_commit/git.py index c24ca86e8..3b97bfd9e 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -91,11 +91,12 @@ def get_conflicted_files(): return set(merge_conflict_filenames) | set(merge_diff_filenames) -def get_staged_files(): +def get_staged_files(cwd=None): return zsplit(cmd_output( 'git', 'diff', '--staged', '--name-only', '--no-ext-diff', '-z', # Everything except for D '--diff-filter=ACMRTUXB', + cwd=cwd, )[1]) diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index 5b50f420c..d9a0401ae 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -123,3 +123,15 @@ def test_try_repo_uncommitted_changes(cap_out, tempdir_factory): config, ) assert rest == 'modified name!...........................................................Passed\n' # noqa: E501 + + +def test_try_repo_staged_changes(tempdir_factory): + repo = make_repo(tempdir_factory, 'modified_file_returns_zero_repo') + + with cwd(repo): + open('staged-file', 'a').close() + open('second-staged-file', 'a').close() + cmd_output('git', 'add', '.') + + with cwd(git_dir(tempdir_factory)): + assert not try_repo(try_repo_opts(repo, hook='bash_hook')) From a18b683d12feb95a966c46ca0e8a78ef62e89f80 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 18 Mar 2019 02:31:47 +0100 Subject: [PATCH 370/544] Add review suggestion Co-Authored-By: DanielChabrowski --- pre_commit/commands/try_repo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index 4bffd7544..e55739e0a 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -38,7 +38,7 @@ def _repo_ref(tmpdir, repo, ref): env = dict(os.environ, GIT_INDEX_FILE=idx, GIT_OBJECT_DIRECTORY=objs) staged_files = git.get_staged_files(cwd=repo) - if (len(staged_files) > 0): + if staged_files: cmd_output('git', 'add', *staged_files, cwd=repo, env=env) cmd_output('git', 'add', '-u', cwd=repo, env=env) From 24a2c3d8db74d2dcec9818fa944a63bc2a66d1f5 Mon Sep 17 00:00:00 2001 From: DanielChabrowski Date: Tue, 19 Mar 2019 08:33:41 +0100 Subject: [PATCH 371/544] Add support for passing cwd and env to xargs --- pre_commit/xargs.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index a382759c4..f32cb32c4 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -109,6 +109,7 @@ def xargs(cmd, varargs, **kwargs): """ negate = kwargs.pop('negate', False) target_concurrency = kwargs.pop('target_concurrency', 1) + max_length = kwargs.pop('_max_length', _get_platform_max_length()) retcode = 0 stdout = b'' stderr = b'' @@ -118,10 +119,10 @@ def xargs(cmd, varargs, **kwargs): except parse_shebang.ExecutableNotFoundError as e: return e.to_output() - partitions = partition(cmd, varargs, target_concurrency, **kwargs) + partitions = partition(cmd, varargs, target_concurrency, max_length) def run_cmd_partition(run_cmd): - return cmd_output(*run_cmd, encoding=None, retcode=None) + return cmd_output(*run_cmd, encoding=None, retcode=None, **kwargs) threads = min(len(partitions), target_concurrency) with _thread_mapper(threads) as thread_map: From 7023caba944a6480d3a83cd4dd8e5d64b70e29dd Mon Sep 17 00:00:00 2001 From: DanielChabrowski Date: Tue, 19 Mar 2019 08:34:30 +0100 Subject: [PATCH 372/544] Execute with xargs in try_repo --- pre_commit/commands/try_repo.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index e55739e0a..3e256ad89 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -15,6 +15,7 @@ from pre_commit.store import Store from pre_commit.util import cmd_output from pre_commit.util import tmpdir +from pre_commit.xargs import xargs logger = logging.getLogger(__name__) @@ -39,7 +40,7 @@ def _repo_ref(tmpdir, repo, ref): staged_files = git.get_staged_files(cwd=repo) if staged_files: - cmd_output('git', 'add', *staged_files, cwd=repo, env=env) + xargs(('git', 'add', '--'), staged_files, cwd=repo, env=env) cmd_output('git', 'add', '-u', cwd=repo, env=env) git.commit(repo=shadow) From c7b369a7be37094e41a1737eb2057caf0245392e Mon Sep 17 00:00:00 2001 From: DanielChabrowski Date: Tue, 19 Mar 2019 09:30:18 +0100 Subject: [PATCH 373/544] Add test for xargs propagating kwargs to cmd_output --- tests/xargs_test.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/xargs_test.py b/tests/xargs_test.py index a6cffd727..71f5454c7 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -208,3 +208,13 @@ def test_thread_mapper_concurrency_uses_threadpoolexecutor_map(): def test_thread_mapper_concurrency_uses_regular_map(): with xargs._thread_mapper(1) as thread_map: assert thread_map is map + + +def test_xargs_propagate_kwargs_to_cmd(): + env = {'PRE_COMMIT_TEST_VAR': 'Pre commit is awesome'} + cmd = ('bash', '-c', 'echo $PRE_COMMIT_TEST_VAR', '--') + cmd = parse_shebang.normalize_cmd(cmd) + + ret, stdout, _ = xargs.xargs(cmd, ('1',), env=env) + assert ret == 0 + assert b'Pre commit is awesome' in stdout From c78b6967cd19174f338eb6164a2897cdf91d3f34 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 21 Mar 2019 18:28:52 -0700 Subject: [PATCH 374/544] Add top level minimum_pre_commit_version --- pre_commit/clientlib.py | 16 ++++++++++++++++ tests/clientlib_test.py | 21 +++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 77b92d4bf..2f16650ae 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -13,6 +13,7 @@ import pre_commit.constants as C from pre_commit.error_handler import FatalError from pre_commit.languages.all import all_languages +from pre_commit.util import parse_version def check_type_tag(tag): @@ -23,6 +24,16 @@ def check_type_tag(tag): ) +def check_min_version(version): + if parse_version(version) > parse_version(C.VERSION): + raise cfgv.ValidationError( + 'pre-commit version {} is required but version {} is installed. ' + 'Perhaps run `pip install --upgrade pre-commit`.'.format( + version, C.VERSION, + ), + ) + + def _make_argparser(filenames_help): parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*', help=filenames_help) @@ -231,6 +242,11 @@ def _entry(modname): ), cfgv.Optional('exclude', cfgv.check_regex, '^$'), cfgv.Optional('fail_fast', cfgv.check_bool, False), + cfgv.Optional( + 'minimum_pre_commit_version', + cfgv.check_and(cfgv.check_string, check_min_version), + '0', + ), ) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 839bcaf9f..a79c5a073 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -3,6 +3,7 @@ import cfgv import pytest +import pre_commit.constants as C from pre_commit.clientlib import check_type_tag from pre_commit.clientlib import CONFIG_HOOK_DICT from pre_commit.clientlib import CONFIG_REPO_DICT @@ -234,3 +235,23 @@ def test_meta_hook_invalid(config_repo): def test_default_language_version_invalid(mapping): with pytest.raises(cfgv.ValidationError): cfgv.validate(mapping, DEFAULT_LANGUAGE_VERSION) + + +def test_minimum_pre_commit_version_failing(): + with pytest.raises(cfgv.ValidationError) as excinfo: + cfg = {'repos': [], 'minimum_pre_commit_version': '999'} + cfgv.validate(cfg, CONFIG_SCHEMA) + assert str(excinfo.value) == ( + '\n' + '==> At Config()\n' + '==> At key: minimum_pre_commit_version\n' + '=====> pre-commit version 999 is required but version {} is ' + 'installed. Perhaps run `pip install --upgrade pre-commit`.'.format( + C.VERSION, + ) + ) + + +def test_minimum_pre_commit_version_passing(): + cfg = {'repos': [], 'minimum_pre_commit_version': '0'} + cfgv.validate(cfg, CONFIG_SCHEMA) From dc28922ccb25c94e6b4dc5f1cfebc0644511af71 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 21 Mar 2019 21:09:33 -0700 Subject: [PATCH 375/544] Run pre-commit autoupdate Committed via https://github.com/asottile/all-repos --- .pre-commit-config.yaml | 10 ++-- pre_commit/commands/autoupdate.py | 16 +++--- pre_commit/commands/install_uninstall.py | 4 +- pre_commit/commands/run.py | 42 +++++++++------- pre_commit/git.py | 34 +++++++------ pre_commit/make_archives.py | 6 +-- tests/clientlib_test.py | 64 +++++++++++++----------- tests/conftest.py | 8 ++- tests/languages/ruby_test.py | 12 ++--- tests/parse_shebang_test.py | 6 +-- tests/repository_test.py | 24 +++++---- 11 files changed, 125 insertions(+), 101 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e7ecdf884..1b87a4068 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: - id: requirements-txt-fixer - id: double-quote-string-fixer - repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.1 + rev: 3.7.7 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-autopep8 @@ -20,20 +20,20 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v1.14.2 + rev: v1.14.4 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v1.11.1 + rev: v1.12.0 hooks: - id: pyupgrade - repo: https://github.com/asottile/reorder_python_imports - rev: v1.3.5 + rev: v1.4.0 hooks: - id: reorder-python-imports language_version: python3 - repo: https://github.com/asottile/add-trailing-comma - rev: v0.7.1 + rev: v1.0.0 hooks: - id: add-trailing-comma - repo: meta diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 99e96050d..11712e17d 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -84,9 +84,11 @@ def _write_new_config_file(path, output): new_contents = ordered_dump(output, **C.YAML_DUMP_KWARGS) lines = original_contents.splitlines(True) - rev_line_indices_reversed = list(reversed([ - i for i, line in enumerate(lines) if REV_LINE_RE.match(line) - ])) + rev_line_indices_reversed = list( + reversed([ + i for i, line in enumerate(lines) if REV_LINE_RE.match(line) + ]), + ) for line in new_contents.splitlines(True): if REV_LINE_RE.match(line): @@ -140,9 +142,11 @@ def autoupdate(config_file, store, tags_only, repos=()): if new_repo_config['rev'] != repo_config['rev']: changed = True - output.write_line('updating {} -> {}.'.format( - repo_config['rev'], new_repo_config['rev'], - )) + output.write_line( + 'updating {} -> {}.'.format( + repo_config['rev'], new_repo_config['rev'], + ), + ) output_repos.append(new_repo_config) else: output.write_line('already up to date.') diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index a6d501abe..7e33961cd 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -53,9 +53,7 @@ def shebang(): # Homebrew/homebrew-core#35825: be more timid about appropriate `PATH` path_choices = [p for p in os.defpath.split(os.pathsep) if p] exe_choices = [ - 'python{}'.format('.'.join( - str(v) for v in sys.version_info[:i] - )) + 'python{}'.format('.'.join(str(v) for v in sys.version_info[:i])) for i in range(3) ] for path, exe in itertools.product(path_choices, exe_choices): diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 651c7f3fd..2f9095227 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -85,30 +85,36 @@ def _run_single_hook(classifier, hook, args, skips, cols): ) if hook.id in skips or hook.alias in skips: - output.write(get_hook_message( - _hook_msg_start(hook, args.verbose), - end_msg=SKIPPED, - end_color=color.YELLOW, - use_color=args.color, - cols=cols, - )) + output.write( + get_hook_message( + _hook_msg_start(hook, args.verbose), + end_msg=SKIPPED, + end_color=color.YELLOW, + use_color=args.color, + cols=cols, + ), + ) return 0 elif not filenames and not hook.always_run: - output.write(get_hook_message( - _hook_msg_start(hook, args.verbose), - postfix=NO_FILES, - end_msg=SKIPPED, - end_color=color.TURQUOISE, - use_color=args.color, - cols=cols, - )) + output.write( + get_hook_message( + _hook_msg_start(hook, args.verbose), + postfix=NO_FILES, + end_msg=SKIPPED, + end_color=color.TURQUOISE, + use_color=args.color, + cols=cols, + ), + ) return 0 # Print the hook and the dots first in case the hook takes hella long to # run. - output.write(get_hook_message( - _hook_msg_start(hook, args.verbose), end_len=6, cols=cols, - )) + output.write( + get_hook_message( + _hook_msg_start(hook, args.verbose), end_len=6, cols=cols, + ), + ) sys.stdout.flush() diff_before = cmd_output( diff --git a/pre_commit/git.py b/pre_commit/git.py index 3b97bfd9e..64e449cbf 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -84,20 +84,24 @@ def get_conflicted_files(): # If they resolved the merge conflict by choosing a mesh of both sides # this will also include the conflicted files tree_hash = cmd_output('git', 'write-tree')[1].strip() - merge_diff_filenames = zsplit(cmd_output( - 'git', 'diff', '--name-only', '--no-ext-diff', '-z', - '-m', tree_hash, 'HEAD', 'MERGE_HEAD', - )[1]) + merge_diff_filenames = zsplit( + cmd_output( + 'git', 'diff', '--name-only', '--no-ext-diff', '-z', + '-m', tree_hash, 'HEAD', 'MERGE_HEAD', + )[1], + ) return set(merge_conflict_filenames) | set(merge_diff_filenames) def get_staged_files(cwd=None): - return zsplit(cmd_output( - 'git', 'diff', '--staged', '--name-only', '--no-ext-diff', '-z', - # Everything except for D - '--diff-filter=ACMRTUXB', - cwd=cwd, - )[1]) + return zsplit( + cmd_output( + 'git', 'diff', '--staged', '--name-only', '--no-ext-diff', '-z', + # Everything except for D + '--diff-filter=ACMRTUXB', + cwd=cwd, + )[1], + ) def intent_to_add_files(): @@ -119,10 +123,12 @@ def get_all_files(): def get_changed_files(new, old): - return zsplit(cmd_output( - 'git', 'diff', '--name-only', '--no-ext-diff', '-z', - '{}...{}'.format(old, new), - )[1]) + return zsplit( + cmd_output( + 'git', 'diff', '--name-only', '--no-ext-diff', '-z', + '{}...{}'.format(old, new), + )[1], + ) def head_rev(remote): diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index 865ef0615..9dd9e5e77 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -58,9 +58,9 @@ def main(argv=None): parser.add_argument('--dest', default='pre_commit/resources') args = parser.parse_args(argv) for archive_name, repo, ref in REPOS: - output.write_line('Making {}.tar.gz for {}@{}'.format( - archive_name, repo, ref, - )) + output.write_line( + 'Making {}.tar.gz for {}@{}'.format(archive_name, repo, ref), + ) make_archive(archive_name, repo, ref, args.dest) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index a79c5a073..2cdc15285 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -33,41 +33,47 @@ def test_check_type_tag_failures(value): @pytest.mark.parametrize( ('config_obj', 'expected'), ( ( - {'repos': [{ - 'repo': 'git@github.com:pre-commit/pre-commit-hooks', - 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', - 'hooks': [{'id': 'pyflakes', 'files': '\\.py$'}], - }]}, + { + 'repos': [{ + 'repo': 'git@github.com:pre-commit/pre-commit-hooks', + 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', + 'hooks': [{'id': 'pyflakes', 'files': '\\.py$'}], + }], + }, True, ), ( - {'repos': [{ - 'repo': 'git@github.com:pre-commit/pre-commit-hooks', - 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', - 'hooks': [ - { - 'id': 'pyflakes', - 'files': '\\.py$', - 'args': ['foo', 'bar', 'baz'], - }, - ], - }]}, + { + 'repos': [{ + 'repo': 'git@github.com:pre-commit/pre-commit-hooks', + 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', + 'hooks': [ + { + 'id': 'pyflakes', + 'files': '\\.py$', + 'args': ['foo', 'bar', 'baz'], + }, + ], + }], + }, True, ), ( - {'repos': [{ - 'repo': 'git@github.com:pre-commit/pre-commit-hooks', - 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', - 'hooks': [ - { - 'id': 'pyflakes', - 'files': '\\.py$', - # Exclude pattern must be a string - 'exclude': 0, - 'args': ['foo', 'bar', 'baz'], - }, - ], - }]}, + { + 'repos': [{ + 'repo': 'git@github.com:pre-commit/pre-commit-hooks', + 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', + 'hooks': [ + { + 'id': 'pyflakes', + 'files': '\\.py$', + # Exclude pattern must be a string + 'exclude': 0, + 'args': ['foo', 'bar', 'baz'], + }, + ], + }], + }, False, ), ), diff --git a/tests/conftest.py b/tests/conftest.py index baaa64c96..50ad76ed8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -33,11 +33,9 @@ def no_warnings(recwarn): message.startswith('Not importing directory ') and ' missing __init__' in message ): - warnings.append('{}:{} {}'.format( - warning.filename, - warning.lineno, - message, - )) + warnings.append( + '{}:{} {}'.format(warning.filename, warning.lineno, message), + ) assert not warnings diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index bcaf0986c..a0b4cfd4b 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -22,9 +22,9 @@ def test_install_rbenv(tempdir_factory): # Should be able to activate using our script and access rbenv cmd_output( 'bash', '-c', - '. {} && rbenv --help'.format(pipes.quote(prefix.path( - 'rbenv-default', 'bin', 'activate', - ))), + '. {} && rbenv --help'.format( + pipes.quote(prefix.path('rbenv-default', 'bin', 'activate')), + ), ) @@ -36,7 +36,7 @@ def test_install_rbenv_with_version(tempdir_factory): # Should be able to activate and use rbenv install cmd_output( 'bash', '-c', - '. {} && rbenv install --help'.format(pipes.quote(prefix.path( - 'rbenv-1.9.3p547', 'bin', 'activate', - ))), + '. {} && rbenv install --help'.format( + pipes.quote(prefix.path('rbenv-1.9.3p547', 'bin', 'activate')), + ), ) diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index bcd6964ba..400a287cb 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -66,9 +66,9 @@ def test_find_executable_path_ext(in_tmpdir): """Windows exports PATHEXT as a list of extensions to automatically add to executables when doing PATH searching. """ - exe_path = os.path.abspath(write_executable( - '/usr/bin/env sh', filename='run.myext', - )) + exe_path = os.path.abspath( + write_executable('/usr/bin/env sh', filename='run.myext'), + ) env_path = {'PATH': os.path.dirname(exe_path)} env_path_ext = dict(env_path, PATHEXT=os.pathsep.join(('.exe', '.myext'))) assert parse_shebang.find_executable('run') is None diff --git a/tests/repository_test.py b/tests/repository_test.py index 32915f1af..d8bfde303 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -291,9 +291,11 @@ def test_additional_rust_cli_dependencies_installed( # A small rust package with no dependencies. config['hooks'][0]['additional_dependencies'] = [dep] hook = _get_hook(config, store, 'rust-hook') - binaries = os.listdir(hook.prefix.path( - helpers.environment_dir(rust.ENVIRONMENT_DIR, C.DEFAULT), 'bin', - )) + binaries = os.listdir( + hook.prefix.path( + helpers.environment_dir(rust.ENVIRONMENT_DIR, C.DEFAULT), 'bin', + ), + ) # normalize for windows binaries = [os.path.splitext(binary)[0] for binary in binaries] assert 'shellharden' in binaries @@ -308,9 +310,11 @@ def test_additional_rust_lib_dependencies_installed( deps = ['shellharden:3.1.0'] config['hooks'][0]['additional_dependencies'] = deps hook = _get_hook(config, store, 'rust-hook') - binaries = os.listdir(hook.prefix.path( - helpers.environment_dir(rust.ENVIRONMENT_DIR, C.DEFAULT), 'bin', - )) + binaries = os.listdir( + hook.prefix.path( + helpers.environment_dir(rust.ENVIRONMENT_DIR, C.DEFAULT), 'bin', + ), + ) # normalize for windows binaries = [os.path.splitext(binary)[0] for binary in binaries] assert 'rust-hello-world' in binaries @@ -507,9 +511,11 @@ def test_additional_golang_dependencies_installed( deps = ['github.com/golang/example/hello'] config['hooks'][0]['additional_dependencies'] = deps hook = _get_hook(config, store, 'golang-hook') - binaries = os.listdir(hook.prefix.path( - helpers.environment_dir(golang.ENVIRONMENT_DIR, C.DEFAULT), 'bin', - )) + binaries = os.listdir( + hook.prefix.path( + helpers.environment_dir(golang.ENVIRONMENT_DIR, C.DEFAULT), 'bin', + ), + ) # normalize for windows binaries = [os.path.splitext(binary)[0] for binary in binaries] assert 'hello' in binaries From cd61269389bd4925d58054995a5b3e06cf367efc Mon Sep 17 00:00:00 2001 From: Tristan Carel Date: Wed, 27 Mar 2019 06:24:47 +0100 Subject: [PATCH 376/544] Do not run legacy script again when this is the one being executed --- pre_commit/resources/hook-tmpl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index f455ca35e..3703b9b9f 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -54,8 +54,10 @@ def _run_legacy(): else: stdin = None - legacy_hook = os.path.join(HERE, '{}.legacy'.format(HOOK_TYPE)) - if os.access(legacy_hook, os.X_OK): + legacy_script = HOOK_TYPE + '.legacy' + is_legacy_executed = os.path.basename(__file__) == legacy_script + legacy_hook = os.path.join(HERE, legacy_script) + if not is_legacy_executed and os.access(legacy_hook, os.X_OK): cmd = _norm_exe(legacy_hook) + (legacy_hook,) + tuple(sys.argv[1:]) proc = subprocess.Popen(cmd, stdin=subprocess.PIPE if stdin else None) proc.communicate(stdin) From ec72cb7260b0822afd0f6a869bc5a28e6ebcd9b5 Mon Sep 17 00:00:00 2001 From: Tristan Carel Date: Fri, 29 Mar 2019 13:55:04 +0100 Subject: [PATCH 377/544] assert that the pre-commit script being executed is not the legacy --- pre_commit/resources/hook-tmpl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 3703b9b9f..4bfb23987 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -57,7 +57,8 @@ def _run_legacy(): legacy_script = HOOK_TYPE + '.legacy' is_legacy_executed = os.path.basename(__file__) == legacy_script legacy_hook = os.path.join(HERE, legacy_script) - if not is_legacy_executed and os.access(legacy_hook, os.X_OK): + assert not is_legacy_executed, __file__ + if os.access(legacy_hook, os.X_OK): cmd = _norm_exe(legacy_hook) + (legacy_hook,) + tuple(sys.argv[1:]) proc = subprocess.Popen(cmd, stdin=subprocess.PIPE if stdin else None) proc.communicate(stdin) From 9f0cfed6005f98d59a4e8d18972e3c1c756aafd3 Mon Sep 17 00:00:00 2001 From: Artem Polishchuk Date: Sat, 30 Mar 2019 19:56:52 +0200 Subject: [PATCH 378/544] Specify env python explicitly. --- pre_commit/commands/install_uninstall.py | 2 +- pre_commit/resources/hook-tmpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 7e33961cd..5f9f5c392 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -107,7 +107,7 @@ def install( before, rest = contents.split(TEMPLATE_START) to_template, after = rest.split(TEMPLATE_END) - before = before.replace('#!/usr/bin/env python', shebang()) + before = before.replace('#!/usr/bin/env python3', shebang()) hook_file.write(before + TEMPLATE_START) for line in to_template.splitlines(): diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index f455ca35e..0b5161817 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 """File generated by pre-commit: https://pre-commit.com""" from __future__ import print_function From bbc3130af224d0d812f25aaed5bda5dbedbe0f55 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 30 Mar 2019 13:24:53 -0700 Subject: [PATCH 379/544] Produce slightly more helpful message --- pre_commit/resources/hook-tmpl | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 4bfb23987..b706d5aec 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -49,15 +49,22 @@ def _norm_exe(exe): def _run_legacy(): + if __file__.endswith('.legacy'): + raise SystemExit( + "bug: pre-commit's script is installed in migration mode\n" + 'run `pre-commit install -f --hook-type {}` to fix this\n\n' + 'Please report this bug at ' + 'https://github.com/pre-commit/pre-commit/issues'.format( + HOOK_TYPE, + ), + ) + if HOOK_TYPE == 'pre-push': stdin = getattr(sys.stdin, 'buffer', sys.stdin).read() else: stdin = None - legacy_script = HOOK_TYPE + '.legacy' - is_legacy_executed = os.path.basename(__file__) == legacy_script - legacy_hook = os.path.join(HERE, legacy_script) - assert not is_legacy_executed, __file__ + legacy_hook = os.path.join(HERE, '{}.legacy'.format(HOOK_TYPE)) if os.access(legacy_hook, os.X_OK): cmd = _norm_exe(legacy_hook) + (legacy_hook,) + tuple(sys.argv[1:]) proc = subprocess.Popen(cmd, stdin=subprocess.PIPE if stdin else None) From 71a740d65dfa18df80643ca07082d2f0f4848847 Mon Sep 17 00:00:00 2001 From: Ben Norquist Date: Tue, 26 Mar 2019 22:31:44 -0700 Subject: [PATCH 380/544] add helpful message and test --- pre_commit/commands/run.py | 9 ++++++++- tests/commands/run_test.py | 31 ++++++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 2f9095227..ed5a01845 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -215,7 +215,14 @@ def _run_hooks(config, hooks, args, environ): if retval and config['fail_fast']: break if retval and args.show_diff_on_failure and git.has_diff(): - output.write_line('All changes made by hooks:') + if args.all_files: + output.write_line( + 'Pre-commit hook(s) made changes. ' + 'If you are seeing this message on CI,' + ' reproduce locally with: pre-commit run --all-files', + ) + else: + output.write_line('All changes made by hooks:') subprocess.call(('git', '--no-pager', 'diff', '--no-ext-diff')) return retval diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index f6efe244c..11a8eea1b 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -178,16 +178,41 @@ def test_global_exclude(cap_out, store, tempdir_factory): assert printed.endswith(expected) -def test_show_diff_on_failure(capfd, cap_out, store, tempdir_factory): +@pytest.mark.parametrize( + ('args', 'expected_out'), + [ + ( + { + 'show_diff_on_failure': True, + }, + b'All changes made by hooks:', + ), + ( + { + 'show_diff_on_failure': True, + 'all_files': True, + }, + b'reproduce locally with: pre-commit run --all-files', + ), + ], +) +def test_show_diff_on_failure( + args, + expected_out, + capfd, + cap_out, + store, + tempdir_factory, +): git_path = make_consuming_repo( tempdir_factory, 'modified_file_returns_zero_repo', ) with cwd(git_path): stage_a_file('bar.py') _test_run( - cap_out, store, git_path, {'show_diff_on_failure': True}, + cap_out, store, git_path, args, # we're only testing the output after running - (), 1, True, + expected_out, 1, True, ) out, _ = capfd.readouterr() assert 'diff --git' in out From 668e6415c036b16f8e173f0e012eb09080d258d4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 30 Mar 2019 14:05:24 -0700 Subject: [PATCH 381/544] Adjust messaging slightly --- pre_commit/commands/run.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index ed5a01845..cfa62ee24 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -217,12 +217,13 @@ def _run_hooks(config, hooks, args, environ): if retval and args.show_diff_on_failure and git.has_diff(): if args.all_files: output.write_line( - 'Pre-commit hook(s) made changes. ' - 'If you are seeing this message on CI,' - ' reproduce locally with: pre-commit run --all-files', + 'pre-commit hook(s) made changes.\n' + 'If you are seeing this message in CI, ' + 'reproduce locally with: `pre-commit run --all-files`.\n' + 'To run `pre-commit` as part of git workflow, use ' + '`pre-commit install`.', ) - else: - output.write_line('All changes made by hooks:') + output.write_line('All changes made by hooks:') subprocess.call(('git', '--no-pager', 'diff', '--no-ext-diff')) return retval From 5169f455c9647f0267501ebb69a1e4e0f32ff4c1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 30 Mar 2019 16:13:03 -0700 Subject: [PATCH 382/544] v1.15.0 --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 129447a40..640c0c695 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,35 @@ +1.15.0 +====== + +### Features +- No longer require being in a `git` repo to run `pre-commit` `clean` / `gc` / + `sample-config`. + - #959 PR by @asottile. +- Improve command line length limit detection. + - #691 issue by @antonbabenko. + - #966 PR by @asottile. +- Use shallow cloning when possible. + - #958 PR by @DanielChabrowski. +- Add `minimum_pre_commit_version` top level key to require a new-enough + version of `pre-commit`. + - #977 PR by @asottile. +- Add helpful CI-friendly message when running + `pre-commit run --all-files --show-diff-on-failure`. + - #982 PR by @bnorquist. + +### Fixes +- Fix `try-repo` for staged untracked changes. + - #973 PR by @DanielChabrowski. +- Fix rpm build by explicitly using `#!/usr/bin/env python3` in hook template. + - #985 issue by @tim77. + - #986 PR by @tim77. +- Guard against infinite recursion when executing legacy hook script. + - #981 PR by @tristan0x. + +### Misc +- Add test for `git.no_git_env()` + - #972 PR by @javabrett. + 1.14.4 ====== diff --git a/setup.cfg b/setup.cfg index 178e492eb..292fc8983 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.14.4 +version = 1.15.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 681d78b6cf4d9bf0ad6f4f36742f29348f865409 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 1 Apr 2019 09:23:42 -0700 Subject: [PATCH 383/544] Bound maxsize by 4096 when SC_ARG_MAX is not present --- pre_commit/xargs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index f32cb32c4..936a5beff 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -25,7 +25,7 @@ def _environ_size(_env=None): def _get_platform_max_length(): # pragma: no cover (platform specific) if os.name == 'posix': maximum = os.sysconf(str('SC_ARG_MAX')) - 2048 - _environ_size() - maximum = min(maximum, 2 ** 17) + maximum = max(min(maximum, 2 ** 17), 2 ** 12) return maximum elif os.name == 'nt': return 2 ** 15 - 2048 # UNICODE_STRING max - headroom From b33f2c40d8de0cd2c55a9b637773172198962832 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 1 Apr 2019 09:44:09 -0700 Subject: [PATCH 384/544] v1.15.1 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 640c0c695..5384f2aa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +1.15.1 +====== + +### Fixes +- Fix command length calculation on posix when `SC_ARG_MAX` is not defined. + - #691 issue by @ushuz. + - #987 PR by @asottile. + 1.15.0 ====== diff --git a/setup.cfg b/setup.cfg index 292fc8983..5538ad4bb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.15.0 +version = 1.15.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From eab24f3e480bceef429d732615b7b0d95d82e940 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 16 Apr 2019 10:30:05 -0700 Subject: [PATCH 385/544] Fix full clone + non-mainline tag --- pre_commit/store.py | 2 +- tests/store_test.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index 93a9cab32..d1d432dc1 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -138,7 +138,7 @@ def _get_result(): def _complete_clone(self, ref, git_cmd): """Perform a complete clone of a repository and its submodules """ - git_cmd('fetch', 'origin') + git_cmd('fetch', 'origin', '--tags') git_cmd('checkout', ref) git_cmd('submodule', 'update', '--init', '--recursive') diff --git a/tests/store_test.py b/tests/store_test.py index 662175880..1833dee73 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -13,6 +13,7 @@ from pre_commit.store import _get_default_directory from pre_commit.store import Store from pre_commit.util import CalledProcessError +from pre_commit.util import cmd_output from testing.fixtures import git_dir from testing.util import cwd from testing.util import git_commit @@ -147,6 +148,20 @@ def fake_shallow_clone(self, *args, **kwargs): assert store.select_all_repos() == [(path, rev, ret)] +def test_clone_tag_not_on_mainline(store, tempdir_factory): + path = git_dir(tempdir_factory) + with cwd(path): + git_commit() + cmd_output('git', 'checkout', 'master', '-b', 'branch') + git_commit() + cmd_output('git', 'tag', 'v1') + cmd_output('git', 'checkout', 'master') + cmd_output('git', 'branch', '-D', 'branch') + + # previously crashed on unreachable refs + store.clone(path, 'v1') + + def test_create_when_directory_exists_but_not_db(store): # In versions <= 0.3.5, there was no sqlite db causing a need for # backward compatibility From 809b7482df7b739014cb583c0793f495d9a949d0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 16 Apr 2019 11:33:16 -0700 Subject: [PATCH 386/544] v1.15.2 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5384f2aa1..09f0fdaf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +1.15.2 +====== + +### Fixes +- Fix cloning non-branch tag while in the fallback slow-clone strategy. + - #997 issue by @jpinner. + - #998 PR by @asottile. + 1.15.1 ====== diff --git a/setup.cfg b/setup.cfg index 5538ad4bb..0e4cf7dea 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.15.1 +version = 1.15.2 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From e60f541559f19b858c499cbe182a9cf1d35c2f53 Mon Sep 17 00:00:00 2001 From: Marc Jay Date: Sun, 21 Apr 2019 21:07:13 +0100 Subject: [PATCH 387/544] Adds support for prepare-commit-msg hooks Adds a prepare-commit-msg hook stage which allows for hooks which add dynamic suggested/placeholder text to commit messages that an author can use as a starting point for writing a commit message --- pre_commit/commands/run.py | 2 +- pre_commit/constants.py | 2 +- pre_commit/main.py | 4 +- pre_commit/resources/hook-tmpl | 1 + tests/commands/install_uninstall_test.py | 62 +++++++++++++++++++++++- tests/commands/run_test.py | 28 ++++++++++- tests/conftest.py | 49 +++++++++++++++++++ 7 files changed, 142 insertions(+), 6 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index cfa62ee24..95488b52b 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -190,7 +190,7 @@ def _compute_cols(hooks, verbose): def _all_filenames(args): if args.origin and args.source: return git.get_changed_files(args.origin, args.source) - elif args.hook_stage == 'commit-msg': + elif args.hook_stage in ['prepare-commit-msg', 'commit-msg']: return (args.commit_msg_filename,) elif args.files: return args.files diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 996480a9a..307b09a45 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -21,6 +21,6 @@ VERSION = importlib_metadata.version('pre_commit') # `manual` is not invoked by any installed git hook. See #719 -STAGES = ('commit', 'commit-msg', 'manual', 'push') +STAGES = ('commit', 'prepare-commit-msg', 'commit-msg', 'manual', 'push') DEFAULT = 'default' diff --git a/pre_commit/main.py b/pre_commit/main.py index a935cf1cf..aa7ff2a79 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -52,7 +52,9 @@ def _add_config_option(parser): def _add_hook_type_option(parser): parser.add_argument( - '-t', '--hook-type', choices=('pre-commit', 'pre-push', 'commit-msg'), + '-t', '--hook-type', choices=( + 'pre-commit', 'pre-push', 'prepare-commit-msg', 'commit-msg', + ), default='pre-commit', ) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 76123d3c9..19d0e7261 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -161,6 +161,7 @@ def _pre_push(stdin): def _opts(stdin): fns = { + 'prepare-commit-msg': lambda _: ('--commit-msg-filename', sys.argv[1]), 'commit-msg': lambda _: ('--commit-msg-filename', sys.argv[1]), 'pre-commit': lambda _: (), 'pre-push': _pre_push, diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index c19aaa440..a216bd5ab 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -655,7 +655,65 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): assert second_line.startswith('Must have "Signed off by:"...') -def test_install_disallow_mising_config(tempdir_factory, store): +def test_prepare_commit_msg_integration_failing( + failing_prepare_commit_msg_repo, tempdir_factory, store, +): + install(C.CONFIG_FILE, store, hook_type='prepare-commit-msg') + retc, out = _get_commit_output(tempdir_factory) + assert retc == 1 + assert out.startswith('Add "Signed off by:"...') + assert out.strip().endswith('...Failed') + + +def test_prepare_commit_msg_integration_passing( + prepare_commit_msg_repo, tempdir_factory, store, +): + install(C.CONFIG_FILE, store, hook_type='prepare-commit-msg') + msg = 'Hi' + retc, out = _get_commit_output(tempdir_factory, msg=msg) + assert retc == 0 + first_line = out.splitlines()[0] + assert first_line.startswith('Add "Signed off by:"...') + assert first_line.endswith('...Passed') + commit_msg_path = os.path.join( + prepare_commit_msg_repo, '.git/COMMIT_EDITMSG', + ) + with io.open(commit_msg_path, 'rt') as f: + assert 'Signed off by: ' in f.read() + + +def test_prepare_commit_msg_legacy( + prepare_commit_msg_repo, tempdir_factory, store, +): + hook_path = os.path.join( + prepare_commit_msg_repo, '.git/hooks/prepare-commit-msg', + ) + mkdirp(os.path.dirname(hook_path)) + with io.open(hook_path, 'w') as hook_file: + hook_file.write( + '#!/usr/bin/env bash\n' + 'set -eu\n' + 'test -e "$1"\n' + 'echo legacy\n', + ) + make_executable(hook_path) + + install(C.CONFIG_FILE, store, hook_type='prepare-commit-msg') + + msg = 'Hi' + retc, out = _get_commit_output(tempdir_factory, msg=msg) + assert retc == 0 + first_line, second_line = out.splitlines()[:2] + assert first_line == 'legacy' + assert second_line.startswith('Add "Signed off by:"...') + commit_msg_path = os.path.join( + prepare_commit_msg_repo, '.git/COMMIT_EDITMSG', + ) + with io.open(commit_msg_path, 'rt') as f: + assert 'Signed off by: ' in f.read() + + +def test_install_disallow_missing_config(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): remove_config_from_repo(path) @@ -668,7 +726,7 @@ def test_install_disallow_mising_config(tempdir_factory, store): assert ret == 1 -def test_install_allow_mising_config(tempdir_factory, store): +def test_install_allow_missing_config(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): remove_config_from_repo(path) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 11a8eea1b..29534648e 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -557,7 +557,12 @@ def test_stages(cap_out, store, repo_with_passing_hook): 'language': 'pygrep', 'stages': [stage], } - for i, stage in enumerate(('commit', 'push', 'manual'), 1) + for i, stage in enumerate( + ( + 'commit', 'push', 'manual', 'prepare-commit-msg', + 'commit-msg', + ), 1, + ) ], } add_config_to_repo(repo_with_passing_hook, config) @@ -575,6 +580,8 @@ def _run_for_stage(stage): assert _run_for_stage('commit').startswith(b'hook 1...') assert _run_for_stage('push').startswith(b'hook 2...') assert _run_for_stage('manual').startswith(b'hook 3...') + assert _run_for_stage('prepare-commit-msg').startswith(b'hook 4...') + assert _run_for_stage('commit-msg').startswith(b'hook 5...') def test_commit_msg_hook(cap_out, store, commit_msg_repo): @@ -593,6 +600,25 @@ def test_commit_msg_hook(cap_out, store, commit_msg_repo): ) +def test_prepare_commit_msg_hook(cap_out, store, prepare_commit_msg_repo): + filename = '.git/COMMIT_EDITMSG' + with io.open(filename, 'w') as f: + f.write('This is the commit message') + + _test_run( + cap_out, + store, + prepare_commit_msg_repo, + {'hook_stage': 'prepare-commit-msg', 'commit_msg_filename': filename}, + expected_outputs=[b'Add "Signed off by:"', b'Passed'], + expected_ret=0, + stage=False, + ) + + with io.open(filename, 'rt') as f: + assert 'Signed off by: ' in f.read() + + def test_local_hook_passes(cap_out, store, repo_with_passing_hook): config = { 'repo': 'local', diff --git a/tests/conftest.py b/tests/conftest.py index 50ad76ed8..e6d7777e0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,7 @@ from pre_commit.logging_handler import logging_handler from pre_commit.store import Store from pre_commit.util import cmd_output +from pre_commit.util import make_executable from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo from testing.fixtures import write_config @@ -134,6 +135,54 @@ def commit_msg_repo(tempdir_factory): yield path +@pytest.fixture +def prepare_commit_msg_repo(tempdir_factory): + path = git_dir(tempdir_factory) + script_name = 'add_sign_off.sh' + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'add-signoff', + 'name': 'Add "Signed off by:"', + 'entry': './{}'.format(script_name), + 'language': 'script', + 'stages': ['prepare-commit-msg'], + }], + } + write_config(path, config) + with cwd(path): + with io.open(script_name, 'w') as script_file: + script_file.write( + '#!/usr/bin/env bash\n' + 'set -eu\n' + 'echo "\nSigned off by: " >> "$1"\n', + ) + make_executable(script_name) + cmd_output('git', 'add', '.') + git_commit(msg=prepare_commit_msg_repo.__name__) + yield path + + +@pytest.fixture +def failing_prepare_commit_msg_repo(tempdir_factory): + path = git_dir(tempdir_factory) + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'add-signoff', + 'name': 'Add "Signed off by:"', + 'entry': '/usr/bin/env bash -c "exit 1"', + 'language': 'system', + 'stages': ['prepare-commit-msg'], + }], + } + write_config(path, config) + with cwd(path): + cmd_output('git', 'add', '.') + git_commit(msg=failing_prepare_commit_msg_repo.__name__) + yield path + + @pytest.fixture(autouse=True, scope='session') def dont_write_to_home_directory(): """pre_commit.store.Store will by default write to the home directory From 64467f6ab9bcffb6ade2d631e32270cc248750e8 Mon Sep 17 00:00:00 2001 From: Marc Jay Date: Sun, 21 Apr 2019 21:54:23 +0100 Subject: [PATCH 388/544] Fix broken test_manifest_hooks test --- tests/repository_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/repository_test.py b/tests/repository_test.py index d8bfde303..a2a9bb576 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -824,7 +824,9 @@ def test_manifest_hooks(tempdir_factory, store): name='Bash hook', pass_filenames=True, require_serial=False, - stages=('commit', 'commit-msg', 'manual', 'push'), + stages=( + 'commit', 'prepare-commit-msg', 'commit-msg', 'manual', 'push', + ), types=['file'], verbose=False, ) From 82969e4ba3623d6e0205a14a13411ff0aae1e197 Mon Sep 17 00:00:00 2001 From: Marc Jay Date: Sun, 21 Apr 2019 21:58:01 +0100 Subject: [PATCH 389/544] Use set rather than list for commit message related stages, remove default file open modes, tidy up bash call for failing hook test --- pre_commit/commands/run.py | 2 +- tests/commands/install_uninstall_test.py | 4 ++-- tests/commands/run_test.py | 2 +- tests/conftest.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 95488b52b..d060e1861 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -190,7 +190,7 @@ def _compute_cols(hooks, verbose): def _all_filenames(args): if args.origin and args.source: return git.get_changed_files(args.origin, args.source) - elif args.hook_stage in ['prepare-commit-msg', 'commit-msg']: + elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}: return (args.commit_msg_filename,) elif args.files: return args.files diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index a216bd5ab..e253dd4bb 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -678,7 +678,7 @@ def test_prepare_commit_msg_integration_passing( commit_msg_path = os.path.join( prepare_commit_msg_repo, '.git/COMMIT_EDITMSG', ) - with io.open(commit_msg_path, 'rt') as f: + with io.open(commit_msg_path) as f: assert 'Signed off by: ' in f.read() @@ -709,7 +709,7 @@ def test_prepare_commit_msg_legacy( commit_msg_path = os.path.join( prepare_commit_msg_repo, '.git/COMMIT_EDITMSG', ) - with io.open(commit_msg_path, 'rt') as f: + with io.open(commit_msg_path) as f: assert 'Signed off by: ' in f.read() diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 29534648e..b465cae6d 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -615,7 +615,7 @@ def test_prepare_commit_msg_hook(cap_out, store, prepare_commit_msg_repo): stage=False, ) - with io.open(filename, 'rt') as f: + with io.open(filename) as f: assert 'Signed off by: ' in f.read() diff --git a/tests/conftest.py b/tests/conftest.py index e6d7777e0..23ff7460a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -171,7 +171,7 @@ def failing_prepare_commit_msg_repo(tempdir_factory): 'hooks': [{ 'id': 'add-signoff', 'name': 'Add "Signed off by:"', - 'entry': '/usr/bin/env bash -c "exit 1"', + 'entry': 'bash -c "exit 1"', 'language': 'system', 'stages': ['prepare-commit-msg'], }], From efeef97f5ee569edb3061f53caa84ab16fdae64f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 21 Apr 2019 15:32:11 -0700 Subject: [PATCH 390/544] passenv %LocalAppData% so go functions on windows --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index f63c3ce5d..d9bcb6b1e 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ envlist = py27,py36,py37,pypy [testenv] deps = -rrequirements-dev.txt -passenv = GOROOT HOME HOMEPATH PROGRAMDATA TERM +passenv = LOCALAPPDATA commands = coverage erase coverage run -m pytest {posargs:tests} From af2c6de9ae0561615cba19585489e1e6925b8722 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 27 Apr 2019 15:10:01 -0700 Subject: [PATCH 391/544] Fix double legacy install on windows --- pre_commit/commands/install_uninstall.py | 3 ++- tests/commands/install_uninstall_test.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 5f9f5c392..701afccb3 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -5,6 +5,7 @@ import itertools import logging import os.path +import shutil import sys from pre_commit import git @@ -84,7 +85,7 @@ def install( # If we have an existing hook, move it to pre-commit.legacy if os.path.lexists(hook_path) and not is_our_script(hook_path): - os.rename(hook_path, legacy_path) + shutil.move(hook_path, legacy_path) # If we specify overwrite, we simply delete the legacy file if overwrite and os.path.exists(legacy_path): diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index e253dd4bb..3bb0a3eac 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -325,6 +325,16 @@ def test_install_existing_hooks_no_overwrite(tempdir_factory, store): assert NORMAL_PRE_COMMIT_RUN.match(output[len('legacy hook\n'):]) +def test_legacy_overwriting_legacy_hook(tempdir_factory, store): + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + with cwd(path): + _write_legacy_hook(path) + assert install(C.CONFIG_FILE, store) == 0 + _write_legacy_hook(path) + # this previously crashed on windows. See #1010 + assert install(C.CONFIG_FILE, store) == 0 + + def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): From 9c6edab726b1b98cd01b7fa2da0d76c125f33909 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 28 Apr 2019 17:36:54 -0700 Subject: [PATCH 392/544] azure pipelines [skip travis] [skip appveyor] --- .travis.yml | 34 ----------------------- README.md | 5 ++-- appveyor.yml | 29 -------------------- azure-pipelines.yml | 50 ++++++++++++++++++++++++++++++++++ pre_commit/languages/node.py | 10 ++++--- pre_commit/languages/python.py | 8 +++--- testing/util.py | 4 +-- tests/xargs_test.py | 2 +- tox.ini | 15 +++++----- 9 files changed, 72 insertions(+), 85 deletions(-) delete mode 100644 .travis.yml delete mode 100644 appveyor.yml create mode 100644 azure-pipelines.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 32376b270..000000000 --- a/.travis.yml +++ /dev/null @@ -1,34 +0,0 @@ -language: python -dist: xenial -services: - - docker -matrix: - include: - - env: TOXENV=py27 - - env: TOXENV=py27 LATEST_GIT=1 - - env: TOXENV=py36 - python: 3.6 - - env: TOXENV=pypy - python: pypy2.7-5.10.0 - - env: TOXENV=py37 - python: 3.7 -install: pip install coveralls tox -script: tox -before_install: - - git --version - - | - if [ "$LATEST_GIT" = "1" ]; then - testing/latest-git.sh - export PATH="/tmp/git/bin:$PATH" - fi - - git --version - - 'testing/get-swift.sh && export PATH="/tmp/swift/usr/bin:$PATH"' - - 'curl -sSf https://sh.rustup.rs | bash -s -- -y' - - export PATH="$HOME/.cargo/bin:$PATH" -after_success: coveralls -cache: - directories: - - $HOME/.cache/pip - - $HOME/.cache/pre-commit - - $HOME/.rustup - - $HOME/.swift diff --git a/README.md b/README.md index 12b222d3b..c91a69ac4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ -[![Build Status](https://travis-ci.org/pre-commit/pre-commit.svg?branch=master)](https://travis-ci.org/pre-commit/pre-commit) -[![Coverage Status](https://coveralls.io/repos/github/pre-commit/pre-commit/badge.svg?branch=master)](https://coveralls.io/github/pre-commit/pre-commit?branch=master) -[![Build status](https://ci.appveyor.com/api/projects/status/mmcwdlfgba4esaii/branch/master?svg=true)](https://ci.appveyor.com/project/asottile/pre-commit/branch/master) +[![Build Status](https://dev.azure.com/asottile/asottile/_apis/build/status/asottile.pyupgrade?branchName=master)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master) +[![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/21/master.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master) ## pre-commit diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 23d3931c6..000000000 --- a/appveyor.yml +++ /dev/null @@ -1,29 +0,0 @@ -environment: - global: - COVERAGE_IGNORE_WINDOWS: '# pragma: windows no cover' - TOX_TESTENV_PASSENV: COVERAGE_IGNORE_WINDOWS - matrix: - - TOXENV: py27 - - TOXENV: py37 - -install: - - "SET PATH=C:\\Python37;C:\\Python37\\Scripts;%PATH%" - - pip install tox virtualenv --upgrade - - "mkdir -p C:\\Temp" - - "SET TMPDIR=C:\\Temp" - - "curl -sSf https://sh.rustup.rs | bash -s -- -y" - - "SET PATH=%USERPROFILE%\\.cargo\\bin;%PATH%" - -# Not a C# project -build: false - -before_test: - # Shut up CRLF messages - - git config --global core.autocrlf false - - git config --global core.safecrlf false - -test_script: tox - -cache: - - '%LOCALAPPDATA%\pip\cache' - - '%USERPROFILE%\.cache\pre-commit' diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 000000000..ce09d9c40 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,50 @@ +trigger: + branches: + include: [master, test-me-*] + tags: + include: ['*'] + +resources: + repositories: + - repository: asottile + type: github + endpoint: github + name: asottile/azure-pipeline-templates + ref: refs/tags/v0.0.13 + +jobs: +- template: job--pre-commit.yml@asottile +- template: job--python-tox.yml@asottile + parameters: + toxenvs: [py27, py37] + os: windows + additional_variables: + COVERAGE_IGNORE_WINDOWS: '# pragma: windows no cover' + TOX_TESTENV_PASSENV: COVERAGE_IGNORE_WINDOWS + TEMP: C:\Temp # remove when dropping python2 + pre_test: + - template: step--rust-install.yml +- template: job--python-tox.yml@asottile + parameters: + toxenvs: [py37] + os: linux + name_postfix: _latest_git + pre_test: + - task: UseRubyVersion@0 + - template: step--git-install.yml + - template: step--rust-install.yml + - bash: | + testing/get-swift.sh + echo '##vso[task.prependpath]/tmp/swift/usr/bin' + displayName: install swift +- template: job--python-tox.yml@asottile + parameters: + toxenvs: [pypy, pypy3, py27, py36, py37] + os: linux + pre_test: + - task: UseRubyVersion@0 + - template: step--rust-install.yml + - bash: | + testing/get-swift.sh + echo '##vso[task.prependpath]/tmp/swift/usr/bin' + displayName: install swift diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index aac1c591d..cd3b7b541 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -23,7 +23,7 @@ def _envdir(prefix, version): return prefix.path(directory) -def get_env_patch(venv): +def get_env_patch(venv): # pragma: windows no cover if sys.platform == 'cygwin': # pragma: no cover _, win_venv, _ = cmd_output('cygpath', '-w', venv) install_prefix = r'{}\bin'.format(win_venv.strip()) @@ -41,12 +41,14 @@ def get_env_patch(venv): @contextlib.contextmanager -def in_env(prefix, language_version): +def in_env(prefix, language_version): # pragma: windows no cover with envcontext(get_env_patch(_envdir(prefix, language_version))): yield -def install_environment(prefix, version, additional_dependencies): +def install_environment( + prefix, version, additional_dependencies, +): # pragma: windows no cover additional_dependencies = tuple(additional_dependencies) assert prefix.exists('package.json') envdir = _envdir(prefix, version) @@ -72,6 +74,6 @@ def install_environment(prefix, version, additional_dependencies): ) -def run_hook(hook, file_args): +def run_hook(hook, file_args): # pragma: windows no cover with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 86f5368cb..2897d0eaf 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -109,15 +109,15 @@ def norm_version(version): if _sys_executable_matches(version): return sys.executable + version_exec = _find_by_py_launcher(version) + if version_exec: + return version_exec + # Try looking up by name version_exec = find_executable(version) if version_exec and version_exec != version: return version_exec - version_exec = _find_by_py_launcher(version) - if version_exec: - return version_exec - # If it is in the form pythonx.x search in the default # place on windows if version.startswith('python'): diff --git a/testing/util.py b/testing/util.py index 156967302..b3b128686 100644 --- a/testing/util.py +++ b/testing/util.py @@ -30,8 +30,8 @@ def cmd_output_mocked_pre_commit_home(*args, **kwargs): skipif_cant_run_docker = pytest.mark.skipif( - docker_is_running() is False, - reason='Docker isn\'t running or can\'t be accessed', + os.name == 'nt' or not docker_is_running(), + reason="Docker isn't running or can't be accessed", ) skipif_cant_run_swift = pytest.mark.skipif( diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 71f5454c7..d2d7d7b35 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -145,7 +145,7 @@ def test_argument_too_long(): def test_xargs_smoke(): ret, out, err = xargs.xargs(('echo',), ('hello', 'world')) assert ret == 0 - assert out == b'hello world\n' + assert out.replace(b'\r\n', b'\n') == b'hello world\n' assert err == b'' diff --git a/tox.ini b/tox.ini index d9bcb6b1e..0ee1611fc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,20 +1,19 @@ [tox] -project = pre_commit -# These should match the travis env list -envlist = py27,py36,py37,pypy +envlist = py27,py36,py37,pypy,pypy3,pre-commit [testenv] deps = -rrequirements-dev.txt -passenv = LOCALAPPDATA +passenv = HOME LOCALAPPDATA commands = coverage erase coverage run -m pytest {posargs:tests} coverage report --fail-under 100 - pre-commit run --all-files + pre-commit install -[testenv:venv] -envdir = venv-{[tox]project} -commands = +[testenv:pre-commit] +skip_install = true +deps = pre-commit +commands = pre-commit run --all-files --show-diff-on-failure [pep8] ignore = E265,E501,W504 From ee80f6218afbf4bcd49f3eee18b65aaea2d10e10 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 28 Apr 2019 22:08:21 -0700 Subject: [PATCH 393/544] Fix badge url --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c91a69ac4..01d0d757a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://dev.azure.com/asottile/asottile/_apis/build/status/asottile.pyupgrade?branchName=master)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master) +[![Build Status](https://dev.azure.com/asottile/asottile/_apis/build/status/pre-commit.pre-commit?branchName=master)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master) [![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/21/master.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master) ## pre-commit From 64a65351b990891ed2ead2177dc4101d1a6983df Mon Sep 17 00:00:00 2001 From: Max R Date: Mon, 29 Apr 2019 01:40:12 -0400 Subject: [PATCH 394/544] Whitespace nit --- testing/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/util.py b/testing/util.py index b3b128686..a030b65c6 100644 --- a/testing/util.py +++ b/testing/util.py @@ -31,7 +31,7 @@ def cmd_output_mocked_pre_commit_home(*args, **kwargs): skipif_cant_run_docker = pytest.mark.skipif( os.name == 'nt' or not docker_is_running(), - reason="Docker isn't running or can't be accessed", + reason="Docker isn't running or can't be accessed", ) skipif_cant_run_swift = pytest.mark.skipif( From e9e665d042cc3bf00b84e9febc27b42cde1f0467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rnar=20Myrheim?= <45852703+Myrheimb@users.noreply.github.com> Date: Mon, 29 Apr 2019 20:22:18 +0200 Subject: [PATCH 395/544] Could this fix #1013? I'm still a beginner, but a single single quote looked a bit off to me. Could adding another single quote after pre fix this issue? --- testing/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/util.py b/testing/util.py index a030b65c6..ecffea695 100644 --- a/testing/util.py +++ b/testing/util.py @@ -67,7 +67,7 @@ def broken_deep_listdir(): # pragma: no cover (platform specific) def platform_supports_pcre(): - output = cmd_output(GREP, '-P', "name='pre", 'setup.py', retcode=None) + output = cmd_output(GREP, '-P', "name='pre'", 'setup.py', retcode=None) return output[0] == 0 and "name='pre_commit'," in output[1] From 5590fc7a5351ef1793952856fc391f7b382430d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rnar=20Myrheim?= <45852703+Myrheimb@users.noreply.github.com> Date: Mon, 29 Apr 2019 21:20:22 +0200 Subject: [PATCH 396/544] New try to fix #1013 Changed the string and file referenced to handle single quotes properly inside pcre greps. --- testing/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/util.py b/testing/util.py index ecffea695..50b45857a 100644 --- a/testing/util.py +++ b/testing/util.py @@ -67,7 +67,7 @@ def broken_deep_listdir(): # pragma: no cover (platform specific) def platform_supports_pcre(): - output = cmd_output(GREP, '-P', "name='pre'", 'setup.py', retcode=None) + output = cmd_output(GREP, '-P', "name=Don't", 'CHANGELOG.md', retcode=None) return output[0] == 0 and "name='pre_commit'," in output[1] From 5977b1125bc05aae5594e00d9d0ecd607ec8f66d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rnar=20Myrheim?= <45852703+Myrheimb@users.noreply.github.com> Date: Mon, 29 Apr 2019 21:33:58 +0200 Subject: [PATCH 397/544] Another try to fix #1013 Properly changed the string and file referenced to handle single quotes properly inside pcre greps. --- testing/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/util.py b/testing/util.py index 50b45857a..91a44bab2 100644 --- a/testing/util.py +++ b/testing/util.py @@ -67,7 +67,7 @@ def broken_deep_listdir(): # pragma: no cover (platform specific) def platform_supports_pcre(): - output = cmd_output(GREP, '-P', "name=Don't", 'CHANGELOG.md', retcode=None) + output = cmd_output(GREP, '-P', "Don't", 'CHANGELOG.md', retcode=None) return output[0] == 0 and "name='pre_commit'," in output[1] From 00e048995c79808f5f6cedb85aec09c73a6eb141 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rnar=20Myrheim?= <45852703+Myrheimb@users.noreply.github.com> Date: Mon, 29 Apr 2019 21:45:56 +0200 Subject: [PATCH 398/544] Yet another try to fix #1013 One again changed the string and file referenced to handle single quotes properly inside pcre greps. --- testing/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/util.py b/testing/util.py index 91a44bab2..d82612fa5 100644 --- a/testing/util.py +++ b/testing/util.py @@ -68,7 +68,7 @@ def broken_deep_listdir(): # pragma: no cover (platform specific) def platform_supports_pcre(): output = cmd_output(GREP, '-P', "Don't", 'CHANGELOG.md', retcode=None) - return output[0] == 0 and "name='pre_commit'," in output[1] + return output[0] == 0 and "Don't use readlink -f" in output[1] xfailif_no_pcre_support = pytest.mark.xfail( From 75651dc8b0bc226bac9b92fef83306792ec87241 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 4 May 2019 08:26:42 -0700 Subject: [PATCH 399/544] v1.16.0 --- CHANGELOG.md | 20 ++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09f0fdaf2..b66fb2ea7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +1.16.0 +====== + +### Features +- Add support for `prepare-commit-msg` hook + - #1004 PR by @marcjay. + +### Fixes +- Fix repeated legacy `pre-commit install` on windows + - #1010 issue by @AbhimanyuHK. + - #1011 PR by @asottile. +- Whitespace fixup + - #1014 PR by @mxr. +- Fix CI check for working pcre support + - #1015 PR by @Myrheimb. + +### Misc. +- Switch CI from travis / appveyor to azure pipelines + - #1012 PR by @asottile. + 1.15.2 ====== diff --git a/setup.cfg b/setup.cfg index 0e4cf7dea..90e365049 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.15.2 +version = 1.16.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From c65dd3ea3af8018695e31abfec38a77a4f66daee Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 4 May 2019 08:39:53 -0700 Subject: [PATCH 400/544] Manually fix up 0.1 tag log --- CHANGELOG.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b66fb2ea7..fc02d0763 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1098,7 +1098,6 @@ that have helped us get this far! ===== - Fixed bug with autoupdate setting defaults on un-updated repos. - -0.1 -=== +0.1.0 +===== - Initial Release From f72a82359c9aae8c63a87bfdb3da79161181dc41 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 4 May 2019 08:43:11 -0700 Subject: [PATCH 401/544] Add dates to changelog entries Automated with this script: ```bash git tag -l | sed 's/^v//g' | xargs --replace bash -c 'sed -r -i "s/^({})$/\1 - $(git show --format=%ad --date=short --no-patch v{})/g" CHANGELOG.md' sed -r -i 's/^(=+)$/\1=============/g' CHANGELOG.md # - 2019-01-01 ``` Thanks @hynek for the suggestion --- CHANGELOG.md | 512 +++++++++++++++++++++++++-------------------------- 1 file changed, 256 insertions(+), 256 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc02d0763..692bf421d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ -1.16.0 -====== +1.16.0 - 2019-05-04 +=================== ### Features - Add support for `prepare-commit-msg` hook @@ -18,24 +18,24 @@ - Switch CI from travis / appveyor to azure pipelines - #1012 PR by @asottile. -1.15.2 -====== +1.15.2 - 2019-04-16 +=================== ### Fixes - Fix cloning non-branch tag while in the fallback slow-clone strategy. - #997 issue by @jpinner. - #998 PR by @asottile. -1.15.1 -====== +1.15.1 - 2019-04-01 +=================== ### Fixes - Fix command length calculation on posix when `SC_ARG_MAX` is not defined. - #691 issue by @ushuz. - #987 PR by @asottile. -1.15.0 -====== +1.15.0 - 2019-03-30 +=================== ### Features - No longer require being in a `git` repo to run `pre-commit` `clean` / `gc` / @@ -66,8 +66,8 @@ - Add test for `git.no_git_env()` - #972 PR by @javabrett. -1.14.4 -====== +1.14.4 - 2019-02-18 +=================== ### Fixes - Don't filter `GIT_SSH_COMMAND` env variable from `git` commands @@ -80,8 +80,8 @@ - #664 issue by @revolter. - #944 PR by @minrk. -1.14.3 -====== +1.14.3 - 2019-02-04 +=================== ### Fixes - Improve performance of filename classification by 45% - 55%. @@ -95,24 +95,24 @@ - Require a newer virtualenv to fix metadata-based setup.cfg installs. - #936 PR by @asottile. -1.14.2 -====== +1.14.2 - 2019-01-10 +=================== ### Fixes - Make the hook shebang detection more timid (1.14.0 regression) - Homebrew/homebrew-core#35825. - #915 PR by @asottile. -1.14.1 -====== +1.14.1 - 2019-01-10 +=================== ### Fixes - Fix python executable lookup on windows when using conda - #913 issue by @dawelter2. - #914 PR by @asottile. -1.14.0 -====== +1.14.0 - 2019-01-08 +=================== ### Features - Add an `alias` configuration value to allow repeated hooks to be @@ -169,8 +169,8 @@ - #894 PR by @s0undt3ch. -1.13.0 -====== +1.13.0 - 2018-12-20 +=================== ### Features - Run hooks in parallel @@ -207,8 +207,8 @@ [`language_version`](https://pre-commit.com/#overriding-language-version). -1.12.0 -====== +1.12.0 - 2018-10-23 +=================== ### Fixes - Install multi-hook repositories only once (performance) @@ -218,8 +218,8 @@ - #840 issue by @RonnyPfannschmidt. - #846 PR by @asottile. -1.11.2 -====== +1.11.2 - 2018-10-10 +=================== ### Fixes - `check-useless-exclude` now considers `types` @@ -229,8 +229,8 @@ - #843 issue by @prem-nuro. - #844 PR by @asottile. -1.11.1 -====== +1.11.1 - 2018-09-22 +=================== ### Fixes - Fix `.git` dir detection in `git<2.5` (regression introduced in @@ -238,8 +238,8 @@ - #831 issue by @mmacpherson. - #832 PR by @asottile. -1.11.0 -====== +1.11.0 - 2018-09-02 +=================== ### Features - Add new `fail` language which always fails @@ -252,8 +252,8 @@ - Don't write ANSI colors on windows when color enabling fails - #819 PR by @jeffreyrack. -1.10.5 -====== +1.10.5 - 2018-08-06 +=================== ### Fixes - Work around `PATH` issue with `brew` `python` on `macos` @@ -263,8 +263,8 @@ - #808 issue by @s0undt3ch. - #809 PR by @asottile. -1.10.4 -====== +1.10.4 - 2018-07-22 +=================== ### Fixes - Replace `yaml.load` with safe alternative @@ -290,39 +290,39 @@ - Test against python3.7 - #789 PR by @expobrain. -1.10.3 -====== +1.10.3 - 2018-07-02 +=================== ### Fixes - Fix `pre-push` during a force push without a fetch - #777 issue by @domenkozar. - #778 PR by @asottile. -1.10.2 -====== +1.10.2 - 2018-06-11 +=================== ### Fixes - pre-commit now invokes hooks with a consistent ordering of filenames - issue by @mxr. - #767 PR by @asottile. -1.10.1 -====== +1.10.1 - 2018-05-28 +=================== ### Fixes - `python_venv` language would leak dependencies when pre-commit was installed in a `-mvirtualenv` virtualenv - #755 #756 issue and PR by @asottile. -1.10.0 -====== +1.10.0 - 2018-05-26 +=================== ### Features - Add support for hooks written in `rust` - #751 PR by @chriskuehl. -1.9.0 -===== +1.9.0 - 2018-05-21 +================== ### Features - Add new `python_venv` language which uses the `venv` module instead of @@ -338,8 +338,8 @@ - #750 PR by @asottile. -1.8.2 -===== +1.8.2 - 2018-03-17 +================== ### Fixes - Fix cloning relative paths (regression in 1.7.0) @@ -347,8 +347,8 @@ - #729 PR by @asottile. -1.8.1 -===== +1.8.1 - 2018-03-12 +================== ### Fixes - Fix integration with go 1.10 and `pkg` directory @@ -358,8 +358,8 @@ - #724 PR by @asottile. -1.8.0 -===== +1.8.0 - 2018-03-11 +================== ### Features - Add a `manual` stage for cli-only interaction @@ -369,8 +369,8 @@ - #716 PR by @tdeo. -1.7.0 -===== +1.7.0 - 2018-03-03 +================== ### Features - pre-commit config validation was split to a separate `cfgv` library @@ -403,8 +403,8 @@ `.pre-commit-config.yaml` file. -1.6.0 -===== +1.6.0 - 2018-02-04 +================== ### Features - Hooks now may have a `verbose` option to produce output even without failure @@ -419,16 +419,16 @@ - #694 PR by @asottile. - #699 PR by @asottile. -1.5.1 -===== +1.5.1 - 2018-01-24 +================== ### Fixes - proper detection for root commit during pre-push - #503 PR by @philipgian. - #692 PR by @samskiter. -1.5.0 -===== +1.5.0 - 2018-01-13 +================== ### Features - pre-commit now supports node hooks on windows. @@ -445,8 +445,8 @@ - #688 PR by @asottile. -1.4.5 -===== +1.4.5 - 2018-01-09 +================== ### Fixes - Fix `local` golang repositories with `additional_dependencies`. @@ -456,37 +456,37 @@ - Replace some string literals with constants - #678 PR by @revolter. -1.4.4 -===== +1.4.4 - 2018-01-07 +================== ### Fixes - Invoke `git diff` without a pager during `--show-diff-on-failure`. - #676 PR by @asottile. -1.4.3 -===== +1.4.3 - 2018-01-02 +================== ### Fixes - `pre-commit` on windows can find pythons at non-hardcoded paths. - #674 PR by @asottile. -1.4.2 -===== +1.4.2 - 2018-01-02 +================== ### Fixes - `pre-commit` no longer clears `GIT_SSH` environment variable when cloning. - #671 PR by @rp-tanium. -1.4.1 -===== +1.4.1 - 2017-11-09 +================== ### Fixes - `pre-commit autoupdate --repo ...` no longer deletes other repos. - #660 issue by @KevinHock. - #661 PR by @KevinHock. -1.4.0 -===== +1.4.0 - 2017-11-08 +================== ### Features - Lazily install repositories. @@ -518,8 +518,8 @@ - #642 PR by @jimmidyson. -1.3.0 -===== +1.3.0 - 2017-10-08 +================== ### Features - Add `pre-commit try-repo` commands @@ -534,8 +534,8 @@ - #589 issue by @sverhagen. - #633 PR by @asottile. -1.2.0 -===== +1.2.0 - 2017-10-03 +================== ### Features - Add `pygrep` language @@ -557,8 +557,8 @@ - Fixes python3.6.2 <=> python3.6.3 virtualenv invalidation - e70825ab by @asottile. -1.1.2 -===== +1.1.2 - 2017-09-20 +================== ### Fixes - pre-commit can successfully install commit-msg hooks @@ -566,8 +566,8 @@ - #623 issue by @sobolevn. - #624 PR by @asottile. -1.1.1 -===== +1.1.1 - 2017-09-17 +================== ### Features - pre-commit also checks the `ssl` module for virtualenv health @@ -578,8 +578,8 @@ - #620 #621 issue by @Lucas-C. - #622 PR by @asottile. -1.1.0 -===== +1.1.0 - 2017-09-11 +================== ### Features - pre-commit configuration gains a `fail_fast` option. @@ -594,8 +594,8 @@ - #281 issue by @asieira. - #617 PR by @asottile. -1.0.1 -===== +1.0.1 - 2017-09-07 +================== ### Fixes - Fix a regression in the return code of `pre-commit autoupdate` @@ -603,8 +603,8 @@ successful. - #614 PR by @asottile. -1.0.0 -===== +1.0.0 - 2017-09-07 +================== pre-commit will now be following [semver](https://semver.org/). Thanks to all of the [contributors](https://github.com/pre-commit/pre-commit/graphs/contributors) that have helped us get this far! @@ -645,32 +645,32 @@ that have helped us get this far! new map format. - Update any references from `~/.pre-commit` to `~/.cache/pre-commit`. -0.18.3 -====== +0.18.3 - 2017-09-06 +=================== - Allow --config to affect `pre-commit install` - Tweak not found error message during `pre-push` / `commit-msg` - Improve node support when running under cygwin. -0.18.2 -====== +0.18.2 - 2017-09-05 +=================== - Fix `--all-files`, detection of staged files, detection of manually edited files during merge conflict, and detection of files to push for non-ascii filenames. -0.18.1 -====== +0.18.1 - 2017-09-04 +=================== - Only mention locking when waiting for a lock. - Fix `IOError` during locking in timeout situtation on windows under python 2. -0.18.0 -====== +0.18.0 - 2017-09-02 +=================== - Add a new `docker_image` language type. `docker_image` is intended to be a lightweight hook type similar to `system` / `script` which allows one to use an existing docker image that provides a hook. `docker_image` hooks can also be used as repository `local` hooks. -0.17.0 -====== +0.17.0 - 2017-08-24 +=================== - Fix typos in help - Allow `commit-msg` hook to be uninstalled - Upgrade the `sample-config` @@ -679,20 +679,20 @@ that have helped us get this far! - Fix installation race condition when multiple `pre-commit` processes would attempt to install the same repository. -0.16.3 -====== +0.16.3 - 2017-08-10 +=================== - autoupdate attempts to maintain config formatting. -0.16.2 -====== +0.16.2 - 2017-08-06 +=================== - Initialize submodules in hook repositories. -0.16.1 -====== +0.16.1 - 2017-08-04 +=================== - Improve node support when running under cygwin. -0.16.0 -====== +0.16.0 - 2017-08-01 +=================== - Remove backward compatibility with repositories providing metadata via `hooks.yaml`. New repositories should provide `.pre-commit-hooks.yaml`. Run `pre-commit autoupdate` to upgrade to the latest repositories. @@ -702,26 +702,26 @@ that have helped us get this far! - Fix crash with unstaged end-of-file crlf additions and the file's lines ended with crlf while git was configured with `core-autocrlf = true`. -0.15.4 -====== +0.15.4 - 2017-07-23 +=================== - Add support for the `commit-msg` git hook -0.15.3 -====== +0.15.3 - 2017-07-20 +=================== - Recover from invalid python virtualenvs -0.15.2 -====== +0.15.2 - 2017-07-09 +=================== - Work around a windows-specific virtualenv bug pypa/virtualenv#1062 This failure mode was introduced in 0.15.1 -0.15.1 -====== +0.15.1 - 2017-07-09 +=================== - Use a more intelligent default language version for python -0.15.0 -====== +0.15.0 - 2017-07-02 +=================== - Add `types` and `exclude_types` for filtering files. These options take an array of "tags" identified for each file. The tags are sourced from [identify](https://github.com/chriskuehl/identify). One can list the tags @@ -730,22 +730,22 @@ that have helped us get this far! - `always_run` + missing `files` also defaults to `files: ''` (previously it defaulted to `'^$'` (this reverses e150921c). -0.14.3 -====== +0.14.3 - 2017-06-28 +=================== - Expose `--origin` and `--source` as `PRE_COMMIT_ORIGIN` and `PRE_COMMIT_SOURCE` environment variables when running as `pre-push`. -0.14.2 -====== +0.14.2 - 2017-06-09 +=================== - Use `--no-ext-diff` when running `git diff` -0.14.1 -====== +0.14.1 - 2017-06-02 +=================== - Don't crash when `always_run` is `True` and `files` is not provided. - Set `VIRTUALENV_NO_DOWNLOAD` when making python virtualenvs. -0.14.0 -====== +0.14.0 - 2017-05-16 +=================== - Add a `pre-commit sample-config` command - Enable ansi color escapes on modern windows - `autoupdate` now defaults to `--tags-only`, use `--bleeding-edge` for the @@ -756,99 +756,99 @@ that have helped us get this far! - Add a `pass_filenames` option to allow disabling automatic filename positional arguments to hooks. -0.13.6 -====== +0.13.6 - 2017-03-27 +=================== - Fix regression in 0.13.5: allow `always_run` and `files` together despite doing nothing. -0.13.5 -====== +0.13.5 - 2017-03-26 +=================== - 0.13.4 contained incorrect files -0.13.4 -====== +0.13.4 - 2017-03-26 +=================== - Add `--show-diff-on-failure` option to `pre-commit run` - Replace `jsonschema` with better error messages -0.13.3 -====== +0.13.3 - 2017-02-23 +=================== - Add `--allow-missing-config` to install: allows `git commit` without a configuration. -0.13.2 -====== +0.13.2 - 2017-02-17 +=================== - Version the local hooks repo - Allow `minimum_pre_commit_version` for local hooks -0.13.1 -====== +0.13.1 - 2017-02-16 +=================== - Fix dummy gem for ruby local hooks -0.13.0 -====== +0.13.0 - 2017-02-16 +=================== - Autoupdate now works even when the current state is broken. - Improve pre-push fileset on new branches - Allow "language local" hooks, hooks which install dependencies using `additional_dependencies` and `language` are now allowed in `repo: local`. -0.12.2 -====== +0.12.2 - 2017-01-27 +=================== - Fix docker hooks on older (<1.12) docker -0.12.1 -====== +0.12.1 - 2017-01-25 +=================== - golang hooks now support additional_dependencies - Added a --tags-only option to pre-commit autoupdate -0.12.0 -====== +0.12.0 - 2017-01-24 +=================== - The new default file for implementing hooks in remote repositories is now .pre-commit-hooks.yaml to encourage repositories to add the metadata. As such, the previous hooks.yaml is now deprecated and generates a warning. - Fix bug with local configuration interfering with ruby hooks - Added support for hooks written in golang. -0.11.0 -====== +0.11.0 - 2017-01-20 +=================== - SwiftPM support. -0.10.1 -====== +0.10.1 - 2017-01-05 +=================== - shlex entry of docker based hooks. - Make shlex behaviour of entry more consistent. -0.10.0 -====== +0.10.0 - 2017-01-04 +=================== - Add an `install-hooks` command similar to `install --install-hooks` but without the `install` side-effects. - Adds support for docker based hooks. -0.9.4 -===== +0.9.4 - 2016-12-05 +================== - Warn when cygwin / python mismatch - Add --config for customizing configuration during run - Update rbenv + plugins to latest versions - pcre hooks now fail when grep / ggrep are not present -0.9.3 -===== +0.9.3 - 2016-11-07 +================== - Fix python hook installation when a strange setup.cfg exists -0.9.2 -===== +0.9.2 - 2016-10-25 +================== - Remove some python2.6 compatibility - UI is no longer sized to terminal width, instead 80 characters or longest necessary width. - Fix inability to create python hook environments when using venv / pyvenv on osx -0.9.1 -===== +0.9.1 - 2016-09-10 +================== - Remove some python2.6 compatibility - Fix staged-files-only with external diff tools -0.9.0 -===== +0.9.0 - 2016-08-31 +================== - Only consider forward diff in changed files - Don't run on staged deleted files that still exist - Autoupdate to tags when available @@ -856,95 +856,95 @@ that have helped us get this far! - Fix crash with staged files containing unstaged lines which have non-utf8 bytes and trailing whitespace -0.8.2 -===== +0.8.2 - 2016-05-20 +================== - Fix a crash introduced in 0.8.0 when an executable was not found -0.8.1 -===== +0.8.1 - 2016-05-17 +================== - Fix regression introduced in 0.8.0 when already using rbenv with no configured ruby hook version -0.8.0 -===== +0.8.0 - 2016-04-11 +================== - Fix --files when running in a subdir - Improve --help a bit - Switch to pyterminalsize for determining terminal size -0.7.6 -===== +0.7.6 - 2016-01-19 +================== - Work under latest virtualenv - No longer create empty directories on windows with latest virtualenv -0.7.5 -===== +0.7.5 - 2016-01-15 +================== - Consider dead symlinks as files when committing -0.7.4 -===== +0.7.4 - 2016-01-12 +================== - Produce error message instead of crashing on non-utf8 installation failure -0.7.3 -===== +0.7.3 - 2015-12-22 +================== - Fix regression introduced in 0.7.1 breaking `git commit -a` -0.7.2 -===== +0.7.2 - 2015-12-22 +================== - Add `always_run` setting for hooks to run even without file changes. -0.7.1 -===== +0.7.1 - 2015-12-19 +================== - Support running pre-commit inside submodules -0.7.0 -===== +0.7.0 - 2015-12-13 +================== - Store state about additional_dependencies for rollforward/rollback compatibility -0.6.8 -===== +0.6.8 - 2015-12-07 +================== - Build as a universal wheel - Allow '.format('-like strings in arguments - Add an option to require a minimum pre-commit version -0.6.7 -===== +0.6.7 - 2015-12-02 +================== - Print a useful message when a hook id is not present - Fix printing of non-ascii with unexpected errors - Print a message when a hook modifies files but produces no output -0.6.6 -===== +0.6.6 - 2015-11-25 +================== - Add `additional_dependencies` to hook configuration. - Fix pre-commit cloning under git 2.6 - Small improvements for windows -0.6.5 -===== +0.6.5 - 2015-11-19 +================== - Allow args for pcre hooks -0.6.4 -===== +0.6.4 - 2015-11-13 +================== - Fix regression introduced in 0.6.3 regarding hooks which make non-utf8 diffs -0.6.3 -===== +0.6.3 - 2015-11-12 +================== - Remove `expected_return_code` - Fail a hook if it makes modifications to the working directory -0.6.2 -===== +0.6.2 - 2015-10-14 +================== - Use --no-ri --no-rdoc instead of --no-document for gem to fix old gem -0.6.1 -===== +0.6.1 - 2015-10-08 +================== - Fix pre-push when pushing something that's already up to date -0.6.0 -===== +0.6.0 - 2015-10-05 +================== - Filter hooks by stage (commit, push). -0.5.5 -===== +0.5.5 - 2015-09-04 +================== - Change permissions a few files - Rename the validate entrypoints - Add --version to some entrypoints @@ -953,151 +953,151 @@ that have helped us get this far! - Suppress complaint about $TERM when no tty is attached - Support pcre hooks on osx through ggrep -0.5.4 -===== +0.5.4 - 2015-07-24 +================== - Allow hooks to produce outputs with arbitrary bytes - Fix pre-commit install when .git/hooks/pre-commit is a dead symlink - Allow an unstaged config when using --files or --all-files -0.5.3 -===== +0.5.3 - 2015-06-15 +================== - Fix autoupdate with "local" hooks - don't purge local hooks. -0.5.2 -===== +0.5.2 - 2015-06-02 +================== - Fix autoupdate with "local" hooks -0.5.1 -===== +0.5.1 - 2015-05-23 +================== - Fix bug with unknown non-ascii hook-id - Avoid crash when .git/hooks is not present in some git clients -0.5.0 -===== +0.5.0 - 2015-05-19 +================== - Add a new "local" hook type for running hooks without remote configuration. - Complain loudly when .pre-commit-config.yaml is unstaged. - Better support for multiple language versions when running hooks. - Allow exclude to be defaulted in repository configuration. -0.4.4 -===== +0.4.4 - 2015-03-29 +================== - Use sys.executable when executing virtualenv -0.4.3 -===== +0.4.3 - 2015-03-25 +================== - Use reset instead of checkout when checkout out hook repo -0.4.2 -===== +0.4.2 - 2015-02-27 +================== - Limit length of xargs arguments to workaround windows xargs bug -0.4.1 -===== +0.4.1 - 2015-02-27 +================== - Don't rename across devices when creating sqlite database -0.4.0 -===== +0.4.0 - 2015-02-27 +================== - Make ^C^C During installation not cause all subsequent runs to fail - Print while installing (instead of while cloning) - Use sqlite to manage repositories (instead of symlinks) - MVP Windows support -0.3.6 -===== +0.3.6 - 2015-02-05 +================== - `args` in venv'd languages are now property quoted. -0.3.5 -===== +0.3.5 - 2015-01-15 +================== - Support running during `pre-push`. See https://pre-commit.com/#advanced 'pre-commit during push'. -0.3.4 -===== +0.3.4 - 2015-01-13 +================== - Allow hook providers to default `args` in `hooks.yaml` -0.3.3 -===== +0.3.3 - 2015-01-06 +================== - Improve message for `CalledProcessError` -0.3.2 -===== +0.3.2 - 2014-10-07 +================== - Fix for `staged_files_only` with color.diff = always #176. -0.3.1 -===== +0.3.1 - 2014-10-03 +================== - Fix error clobbering #174. - Remove dependency on `plumbum`. - Allow pre-commit to be run from anywhere in a repository #175. -0.3.0 -===== +0.3.0 - 2014-09-18 +================== - Add `--files` option to `pre-commit run` -0.2.11 -====== +0.2.11 - 2014-09-05 +=================== - Fix terminal width detection (broken in 0.2.10) -0.2.10 -====== +0.2.10 - 2014-09-04 +=================== - Bump version of nodeenv to fix bug with ~/.npmrc - Choose `python` more intelligently when running. -0.2.9 -===== +0.2.9 - 2014-09-02 +================== - Fix bug where sys.stdout.write must take `bytes` in python 2.6 -0.2.8 -===== +0.2.8 - 2014-08-13 +================== - Allow a client to have duplicates of hooks. - Use --prebuilt instead of system for node. - Improve some fatal error messages -0.2.7 -===== +0.2.7 - 2014-07-28 +================== - Produce output when running pre-commit install --install-hooks -0.2.6 -===== +0.2.6 - 2014-07-28 +================== - Print hookid on failure - Use sys.executable for running nodeenv - Allow running as `python -m pre_commit` -0.2.5 -===== +0.2.5 - 2014-07-17 +================== - Default columns to 80 (for non-terminal execution). -0.2.4 -===== +0.2.4 - 2014-07-07 +================== - Support --install-hooks as an argument to `pre-commit install` - Install hooks before attempting to run anything - Use `python -m nodeenv` instead of `nodeenv` -0.2.3 -===== +0.2.3 - 2014-06-25 +================== - Freeze ruby building infrastructure - Fix bug that assumed diffs were utf-8 -0.2.2 -===== +0.2.2 - 2014-06-22 +================== - Fix filenames with spaces -0.2.1 -===== +0.2.1 - 2014-06-18 +================== - Use either `pre-commit` or `python -m pre_commit.main` depending on which is available - Don't use readlink -f -0.2.0 -===== +0.2.0 - 2014-06-17 +================== - Fix for merge-conflict during cherry-picking. - Add -V / --version - Add migration install mode / install -f / --overwrite - Add `pcre` "language" for perl compatible regexes - Reorganize packages. -0.1.1 -===== +0.1.1 - 2014-06-11 +================== - Fixed bug with autoupdate setting defaults on un-updated repos. -0.1.0 -===== +0.1.0 - 2014-06-07 +================== - Initial Release From d74ee6d74305a4f7386fc3c83ca8a24b43389a2d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 7 May 2019 09:38:17 -0700 Subject: [PATCH 402/544] Don't attempt to decode the healthy response --- pre_commit/languages/python.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 2897d0eaf..ca1146700 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -140,6 +140,7 @@ def healthy(prefix, language_version): 'python', '-c', 'import ctypes, datetime, io, os, ssl, weakref', retcode=None, + encoding=None, ) return retcode == 0 From 168ede2be0ead8f88594fa2642e3aa91ac440404 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 8 May 2019 08:26:15 -0700 Subject: [PATCH 403/544] v1.16.1 --- CHANGELOG.md | 9 +++++++++ setup.cfg | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 692bf421d..b4138f486 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +1.16.1 - 2019-05-08 +=================== + +### Fixes +- Don't ``UnicodeDecodeError`` on unexpected non-UTF8 output in python health + check on windows. + - #1021 issue by @nicoddemus. + - #1022 PR by @asottile. + 1.16.0 - 2019-05-04 =================== diff --git a/setup.cfg b/setup.cfg index 90e365049..a87108d5b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.16.0 +version = 1.16.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From b3bfecde3932c271e5f633b6723ed4cb03f0e0e3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 8 May 2019 08:27:53 -0700 Subject: [PATCH 404/544] Fix markdown typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4138f486..79629b797 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ =================== ### Fixes -- Don't ``UnicodeDecodeError`` on unexpected non-UTF8 output in python health +- Don't `UnicodeDecodeError` on unexpected non-UTF8 output in python health check on windows. - #1021 issue by @nicoddemus. - #1022 PR by @asottile. From fd9d9d276b0bf727c48bd720248d88ff915686d8 Mon Sep 17 00:00:00 2001 From: Yoav Caspi Date: Sat, 11 May 2019 20:43:12 +0300 Subject: [PATCH 405/544] Add warning to additional keys in config --- pre_commit/clientlib.py | 17 +++++++++++++++++ tests/clientlib_test.py | 23 +++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 2f16650ae..a16a73ac8 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -3,6 +3,7 @@ import argparse import functools +import logging import pipes import sys @@ -15,6 +16,8 @@ from pre_commit.languages.all import all_languages from pre_commit.util import parse_version +logger = logging.getLogger('pre_commit') + def check_type_tag(tag): if tag not in ALL_TAGS: @@ -144,6 +147,16 @@ def _entry(modname): ) +def warn_on_unknown_keys_at_top_level(extra, orig_keys): + logger.warning( + 'Your pre-commit-config contain these extra keys: {}. ' + 'while the only valid keys are: {}.'.format( + ', '.join(extra), + ', '.join(sorted(orig_keys)), + ), + ), + + _meta = ( ( 'check-hooks-apply', ( @@ -222,6 +235,10 @@ def _entry(modname): ), MigrateShaToRev(), + cfgv.WarnAdditionalKeys( + {'repo', 'rev', 'hooks'}, + warn_on_unknown_keys_at_top_level, + ), ) DEFAULT_LANGUAGE_VERSION = cfgv.Map( 'DefaultLanguageVersion', None, diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 2cdc15285..069dca361 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import logging + import cfgv import pytest @@ -116,6 +118,27 @@ def test_validate_config_old_list_format_ok(tmpdir): assert not validate_config_main((f.strpath,)) +def test_validate_warn_on_unknown_keys_at_top_level(tmpdir, caplog): + f = tmpdir.join('cfg.yaml') + f.write( + '- repo: https://gitlab.com/pycqa/flake8\n' + ' rev: 3.7.7\n' + ' hooks:\n' + ' - id: flake8\n' + ' args: [--some-args]\n', + ) + ret_val = validate_config_main((f.strpath,)) + assert not ret_val + assert caplog.record_tuples == [ + ( + 'pre_commit', + logging.WARNING, + 'Your pre-commit-config contain these extra keys: args. ' + 'while the only valid keys are: hooks, repo, rev.', + ), + ] + + @pytest.mark.parametrize('fn', (validate_config_main, validate_manifest_main)) def test_mains_not_ok(tmpdir, fn): not_yaml = tmpdir.join('f.notyaml') From 59c282b1840c65392b5c299ff7d999ac5b6ce194 Mon Sep 17 00:00:00 2001 From: Yoav Caspi Date: Sat, 11 May 2019 21:47:26 +0300 Subject: [PATCH 406/544] typo fix --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ad7bf01fc..bb875ce75 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ - ruby + gem - docker -### Setting up an environemnt +### Setting up an environment This is useful for running specific tests. The easiest way to set this up is to run: From f21316ebe8e131d1ad5bc3d457e181dec4329bd0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 11 May 2019 12:19:00 -0700 Subject: [PATCH 407/544] Improve output when interrupted (^C) --- pre_commit/error_handler.py | 14 ++++++++------ tests/error_handler_test.py | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 3b0a4c517..946f134cb 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -44,9 +44,11 @@ def _log_and_exit(msg, exc, formatted): def error_handler(): try: yield - except FatalError as e: - _log_and_exit('An error has occurred', e, traceback.format_exc()) - except Exception as e: - _log_and_exit( - 'An unexpected error has occurred', e, traceback.format_exc(), - ) + except (Exception, KeyboardInterrupt) as e: + if isinstance(e, FatalError): + msg = 'An error has occurred' + elif isinstance(e, KeyboardInterrupt): + msg = 'Interrupted (^C)' + else: + msg = 'An unexpected error has occurred' + _log_and_exit(msg, e, traceback.format_exc()) diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 6aebe5a3b..1b222f904 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -73,6 +73,29 @@ def test_error_handler_uncaught_error(mocked_log_and_exit): ) +def test_error_handler_keyboardinterrupt(mocked_log_and_exit): + exc = KeyboardInterrupt() + with error_handler.error_handler(): + raise exc + + mocked_log_and_exit.assert_called_once_with( + 'Interrupted (^C)', + exc, + # Tested below + mock.ANY, + ) + assert re.match( + r'Traceback \(most recent call last\):\n' + r' File ".+pre_commit.error_handler.py", line \d+, in error_handler\n' + r' yield\n' + r' File ".+tests.error_handler_test.py", line \d+, ' + r'in test_error_handler_keyboardinterrupt\n' + r' raise exc\n' + r'KeyboardInterrupt\n', + mocked_log_and_exit.call_args[0][2], + ) + + def test_log_and_exit(cap_out, mock_store_dir): with pytest.raises(SystemExit): error_handler._log_and_exit( From 217d31ec1cfe2ed67d360016d8b3f13e1ee16c1f Mon Sep 17 00:00:00 2001 From: Yoav Caspi Date: Sat, 11 May 2019 22:57:52 +0300 Subject: [PATCH 408/544] Add a check and test to the real top level and improve the warning message --- pre_commit/clientlib.py | 14 ++++++++------ tests/clientlib_test.py | 27 ++++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index a16a73ac8..3285a48b2 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -149,12 +149,10 @@ def _entry(modname): def warn_on_unknown_keys_at_top_level(extra, orig_keys): logger.warning( - 'Your pre-commit-config contain these extra keys: {}. ' - 'while the only valid keys are: {}.'.format( - ', '.join(extra), - ', '.join(sorted(orig_keys)), + 'Unexpected config key(s): {}'.format( + ', '.join(sorted(extra)), ), - ), + ) _meta = ( @@ -236,7 +234,7 @@ def warn_on_unknown_keys_at_top_level(extra, orig_keys): MigrateShaToRev(), cfgv.WarnAdditionalKeys( - {'repo', 'rev', 'hooks'}, + ('repo', 'rev', 'hooks'), warn_on_unknown_keys_at_top_level, ), ) @@ -264,6 +262,10 @@ def warn_on_unknown_keys_at_top_level(extra, orig_keys): cfgv.check_and(cfgv.check_string, check_min_version), '0', ), + cfgv.WarnAdditionalKeys( + ('repos',), + warn_on_unknown_keys_at_top_level, + ), ) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 069dca361..cace0f329 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -118,7 +118,7 @@ def test_validate_config_old_list_format_ok(tmpdir): assert not validate_config_main((f.strpath,)) -def test_validate_warn_on_unknown_keys_at_top_level(tmpdir, caplog): +def test_validate_warn_on_unknown_keys_at_repo_level(tmpdir, caplog): f = tmpdir.join('cfg.yaml') f.write( '- repo: https://gitlab.com/pycqa/flake8\n' @@ -133,8 +133,29 @@ def test_validate_warn_on_unknown_keys_at_top_level(tmpdir, caplog): ( 'pre_commit', logging.WARNING, - 'Your pre-commit-config contain these extra keys: args. ' - 'while the only valid keys are: hooks, repo, rev.', + 'Unexpected config key(s): args', + ), + ] + + +def test_validate_warn_on_unknown_keys_at_top_level(tmpdir, caplog): + f = tmpdir.join('cfg.yaml') + f.write( + 'repos:\n' + '- repo: https://gitlab.com/pycqa/flake8\n' + ' rev: 3.7.7\n' + ' hooks:\n' + ' - id: flake8\n' + 'foo:\n' + ' id: 1.0.0\n', + ) + ret_val = validate_config_main((f.strpath,)) + assert not ret_val + assert caplog.record_tuples == [ + ( + 'pre_commit', + logging.WARNING, + 'Unexpected config key(s): foo', ), ] From ba7760b705f9f0d31215aceab5c3c7bde1ee6447 Mon Sep 17 00:00:00 2001 From: Yoav Caspi Date: Sun, 12 May 2019 15:09:15 +0300 Subject: [PATCH 409/544] Add a test to validate that cfgv.WarnAdditionalKeys working as expected in the relevant config schemas --- pre_commit/clientlib.py | 11 ++++++++++- tests/clientlib_test.py | 9 +++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 3285a48b2..3ceefb1b4 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -109,6 +109,8 @@ def validate_manifest_main(argv=None): class MigrateShaToRev(object): + key = 'rev' + @staticmethod def _cond(key): return cfgv.Conditional( @@ -263,7 +265,14 @@ def warn_on_unknown_keys_at_top_level(extra, orig_keys): '0', ), cfgv.WarnAdditionalKeys( - ('repos',), + ( + 'repos', + 'default_language_version', + 'default_stages', + 'exclude', + 'fail_fast', + 'minimum_pre_commit_version', + ), warn_on_unknown_keys_at_top_level, ), ) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index cace0f329..13b42a597 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -305,3 +305,12 @@ def test_minimum_pre_commit_version_failing(): def test_minimum_pre_commit_version_passing(): cfg = {'repos': [], 'minimum_pre_commit_version': '0'} cfgv.validate(cfg, CONFIG_SCHEMA) + + +@pytest.mark.parametrize('schema', (CONFIG_SCHEMA, CONFIG_REPO_DICT)) +def test_warn_additional(schema): + allowed_keys = {item.key for item in schema.items if hasattr(item, 'key')} + warn_additional, = [ + x for x in schema.items if isinstance(x, cfgv.WarnAdditionalKeys) + ] + assert allowed_keys == set(warn_additional.keys) From 7a998a091e9160ce612cb21f1bd35fd0573ef05b Mon Sep 17 00:00:00 2001 From: Yoav Caspi Date: Sun, 12 May 2019 23:29:42 +0300 Subject: [PATCH 410/544] improve function name --- pre_commit/clientlib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 3ceefb1b4..c16a3ace9 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -149,7 +149,7 @@ def _entry(modname): ) -def warn_on_unknown_keys_at_top_level(extra, orig_keys): +def warn_unknown_keys(extra, orig_keys): logger.warning( 'Unexpected config key(s): {}'.format( ', '.join(sorted(extra)), @@ -237,7 +237,7 @@ def warn_on_unknown_keys_at_top_level(extra, orig_keys): MigrateShaToRev(), cfgv.WarnAdditionalKeys( ('repo', 'rev', 'hooks'), - warn_on_unknown_keys_at_top_level, + warn_unknown_keys, ), ) DEFAULT_LANGUAGE_VERSION = cfgv.Map( @@ -273,7 +273,7 @@ def warn_on_unknown_keys_at_top_level(extra, orig_keys): 'fail_fast', 'minimum_pre_commit_version', ), - warn_on_unknown_keys_at_top_level, + warn_unknown_keys, ), ) From fb15fa65f20e7c618032293ece1bda238672ab43 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 10 May 2019 20:27:52 -0700 Subject: [PATCH 411/544] Fix handling of SIGINT in hook script --- pre_commit/resources/hook-tmpl | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 19d0e7261..a145c8ee8 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -170,16 +170,25 @@ def _opts(stdin): return ('--config', CONFIG, '--hook-stage', stage) + fns[HOOK_TYPE](stdin) +if sys.version_info < (3, 7): # https://bugs.python.org/issue25942 + def _subprocess_call(cmd): # this is the python 2.7 implementation + return subprocess.Popen(cmd).wait() +else: + _subprocess_call = subprocess.call + + def main(): retv, stdin = _run_legacy() try: _validate_config() - return retv | subprocess.call(_exe() + _opts(stdin)) + return retv | _subprocess_call(_exe() + _opts(stdin)) except EarlyExit: return retv except FatalError as e: print(e.args[0]) return 1 + except KeyboardInterrupt: + return 1 if __name__ == '__main__': From 471fe7d58f99f7276cfe8f77803a8d5227f7c85f Mon Sep 17 00:00:00 2001 From: Yoav Caspi Date: Mon, 13 May 2019 23:14:41 +0300 Subject: [PATCH 412/544] restore testenv:venv section. to be able to set the dev environment as described in the CONTRIBUTING.md --- tox.ini | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tox.ini b/tox.ini index 0ee1611fc..105cca6c1 100644 --- a/tox.ini +++ b/tox.ini @@ -1,4 +1,5 @@ [tox] +project = pre_commit envlist = py27,py36,py37,pypy,pypy3,pre-commit [testenv] @@ -10,6 +11,10 @@ commands = coverage report --fail-under 100 pre-commit install +[testenv:venv] +envdir = venv-{[tox]project} +commands = + [testenv:pre-commit] skip_install = true deps = pre-commit From bb78de09d1987f46e1b7eb6286abb5425bec7b70 Mon Sep 17 00:00:00 2001 From: Yoav Caspi Date: Tue, 14 May 2019 00:08:54 +0300 Subject: [PATCH 413/544] move testenv:venv section to be in lexicographic order --- tox.ini | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tox.ini b/tox.ini index 105cca6c1..a63b6533c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,4 @@ [tox] -project = pre_commit envlist = py27,py36,py37,pypy,pypy3,pre-commit [testenv] @@ -11,15 +10,15 @@ commands = coverage report --fail-under 100 pre-commit install -[testenv:venv] -envdir = venv-{[tox]project} -commands = - [testenv:pre-commit] skip_install = true deps = pre-commit commands = pre-commit run --all-files --show-diff-on-failure +[testenv:venv] +envdir = venv-pre_commit +commands = + [pep8] ignore = E265,E501,W504 From da44d4267e7d298b4f662b1bdda2f2edac59ad33 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 22 May 2019 11:04:35 -0700 Subject: [PATCH 414/544] Fix rmtree for readonly directories --- pre_commit/util.py | 5 +++-- tests/util_test.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index 4c3902897..eb5411fdd 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -158,13 +158,14 @@ def cmd_output(*cmd, **kwargs): def rmtree(path): """On windows, rmtree fails for readonly dirs.""" - def handle_remove_readonly(func, path, exc): # pragma: no cover (windows) + def handle_remove_readonly(func, path, exc): excvalue = exc[1] if ( func in (os.rmdir, os.remove, os.unlink) and excvalue.errno == errno.EACCES ): - os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) + for p in (path, os.path.dirname(path)): + os.chmod(p, os.stat(p).st_mode | stat.S_IWUSR) func(path) else: raise diff --git a/tests/util_test.py b/tests/util_test.py index 94c6ae630..c9838c555 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import os.path +import stat import pytest @@ -8,6 +9,7 @@ from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import parse_version +from pre_commit.util import rmtree from pre_commit.util import tmpdir @@ -90,3 +92,14 @@ def test_parse_version(): assert parse_version('0.0') == parse_version('0.0') assert parse_version('0.1') > parse_version('0.0') assert parse_version('2.1') >= parse_version('2') + + +def test_rmtree_read_only_directories(tmpdir): + """Simulates the go module tree. See #1042""" + tmpdir.join('x/y/z').ensure_dir().join('a').ensure() + mode = os.stat(str(tmpdir.join('x'))).st_mode + mode_no_w = mode & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH) + tmpdir.join('x/y/z').chmod(mode_no_w) + tmpdir.join('x/y/z').chmod(mode_no_w) + tmpdir.join('x/y/z').chmod(mode_no_w) + rmtree(str(tmpdir.join('x'))) From e868add5a30e03c8864ece10eaab504529746117 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 26 May 2019 13:12:37 -0700 Subject: [PATCH 415/544] Fix test_environment_not_sourced when pre-commit is installed globally --- azure-pipelines.yml | 2 +- tests/commands/install_uninstall_test.py | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index ce09d9c40..0c7c25952 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -10,7 +10,7 @@ resources: type: github endpoint: github name: asottile/azure-pipeline-templates - ref: refs/tags/v0.0.13 + ref: refs/tags/v0.0.14 jobs: - template: job--pre-commit.yml@asottile diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 3bb0a3eac..5fdf94991 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -18,6 +18,7 @@ from pre_commit.commands.install_uninstall import PRIOR_HASHES from pre_commit.commands.install_uninstall import shebang from pre_commit.commands.install_uninstall import uninstall +from pre_commit.parse_shebang import find_executable from pre_commit.util import cmd_output from pre_commit.util import make_executable from pre_commit.util import mkdirp @@ -234,10 +235,16 @@ def test_install_idempotent(tempdir_factory, store): def _path_without_us(): # Choose a path which *probably* doesn't include us - return os.pathsep.join([ - x for x in os.environ['PATH'].split(os.pathsep) - if x.lower() != os.path.dirname(sys.executable).lower() - ]) + env = dict(os.environ) + exe = find_executable('pre-commit', _environ=env) + while exe: + parts = env['PATH'].split(os.pathsep) + after = [x for x in parts if x.lower() != os.path.dirname(exe).lower()] + if parts == after: + raise AssertionError(exe, parts) + env['PATH'] = os.pathsep.join(after) + exe = find_executable('pre-commit', _environ=env) + return env['PATH'] def test_environment_not_sourced(tempdir_factory, store): From 625750eeef30dbdc36fbed2d4e574cafa169efc4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 27 May 2019 13:37:49 -0700 Subject: [PATCH 416/544] fixes for cfgv>=2 --- pre_commit/clientlib.py | 19 +++++++++++-------- pre_commit/repository.py | 2 +- setup.cfg | 2 +- tests/clientlib_test.py | 5 +++-- tests/repository_test.py | 2 +- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index c16a3ace9..14a22b990 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -149,10 +149,16 @@ def _entry(modname): ) -def warn_unknown_keys(extra, orig_keys): +def warn_unknown_keys_root(extra, orig_keys, dct): logger.warning( - 'Unexpected config key(s): {}'.format( - ', '.join(sorted(extra)), + 'Unexpected key(s) present at root: {}'.format(', '.join(extra)), + ) + + +def warn_unknown_keys_repo(extra, orig_keys, dct): + logger.warning( + 'Unexpected key(s) present on {}: {}'.format( + dct['repo'], ', '.join(extra), ), ) @@ -235,10 +241,7 @@ def warn_unknown_keys(extra, orig_keys): ), MigrateShaToRev(), - cfgv.WarnAdditionalKeys( - ('repo', 'rev', 'hooks'), - warn_unknown_keys, - ), + cfgv.WarnAdditionalKeys(('repo', 'rev', 'hooks'), warn_unknown_keys_repo), ) DEFAULT_LANGUAGE_VERSION = cfgv.Map( 'DefaultLanguageVersion', None, @@ -273,7 +276,7 @@ def warn_unknown_keys(extra, orig_keys): 'fail_fast', 'minimum_pre_commit_version', ), - warn_unknown_keys, + warn_unknown_keys_root, ), ) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 1d92d7531..5b12a98c8 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -105,7 +105,7 @@ def create(cls, src, prefix, dct): extra_keys = set(dct) - set(_KEYS) if extra_keys: logger.warning( - 'Unexpected keys present on {} => {}: ' + 'Unexpected key(s) present on {} => {}: ' '{}'.format(src, dct['id'], ', '.join(sorted(extra_keys))), ) return cls(src=src, prefix=prefix, **{k: dct[k] for k in _KEYS}) diff --git a/setup.cfg b/setup.cfg index a87108d5b..eca74cc8d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,7 +25,7 @@ classifiers = packages = find: install_requires = aspy.yaml - cfgv>=1.4.0 + cfgv>=2.0.0 identify>=1.0.0 importlib-metadata nodeenv>=0.11.1 diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 13b42a597..6174889a3 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -133,7 +133,8 @@ def test_validate_warn_on_unknown_keys_at_repo_level(tmpdir, caplog): ( 'pre_commit', logging.WARNING, - 'Unexpected config key(s): args', + 'Unexpected key(s) present on https://gitlab.com/pycqa/flake8: ' + 'args', ), ] @@ -155,7 +156,7 @@ def test_validate_warn_on_unknown_keys_at_top_level(tmpdir, caplog): ( 'pre_commit', logging.WARNING, - 'Unexpected config key(s): foo', + 'Unexpected key(s) present at root: foo', ), ] diff --git a/tests/repository_test.py b/tests/repository_test.py index a2a9bb576..97fcba052 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -588,7 +588,7 @@ def test_unknown_keys(store, fake_log_handler): }], } _get_hook(config, store, 'too-much') - expected = 'Unexpected keys present on local => too-much: foo, hello' + expected = 'Unexpected key(s) present on local => too-much: foo, hello' assert fake_log_handler.handle.call_args[0][0].msg == expected From 4f4767c9e07039b2885b8610fec99a1def96e845 Mon Sep 17 00:00:00 2001 From: Mandar Vaze Date: Fri, 31 May 2019 16:42:16 +0530 Subject: [PATCH 417/544] Pass color option to git diff (on failure) Fixes #1007 --- pre_commit/commands/run.py | 9 ++++++++- tests/commands/run_test.py | 7 +++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index d060e1861..3c18dd569 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -224,7 +224,14 @@ def _run_hooks(config, hooks, args, environ): '`pre-commit install`.', ) output.write_line('All changes made by hooks:') - subprocess.call(('git', '--no-pager', 'diff', '--no-ext-diff')) + if args.color: + subprocess.call(( + 'git', '--no-pager', 'diff', '--no-ext-diff', + '--color={}'.format(args.color), + )) + else: + subprocess.call(('git', '--no-pager', 'diff', '--no-ext-diff')) + return retval diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index b465cae6d..b4548f6fb 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -187,6 +187,13 @@ def test_global_exclude(cap_out, store, tempdir_factory): }, b'All changes made by hooks:', ), + ( + { + 'show_diff_on_failure': True, + 'color': 'auto', + }, + b'All changes made by hooks:', + ), ( { 'show_diff_on_failure': True, From e08d373be35b5970e0289c8ef5ca49f849d35476 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 31 May 2019 08:47:58 -0700 Subject: [PATCH 418/544] azure pipelines now has rust by default on windows --- azure-pipelines.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 0c7c25952..05ed0c3cd 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -20,10 +20,8 @@ jobs: os: windows additional_variables: COVERAGE_IGNORE_WINDOWS: '# pragma: windows no cover' - TOX_TESTENV_PASSENV: COVERAGE_IGNORE_WINDOWS + TOX_TESTENV_PASSENV: COVERAGE_IGNORE_WINDOWS RUSTUP_HOME TEMP: C:\Temp # remove when dropping python2 - pre_test: - - template: step--rust-install.yml - template: job--python-tox.yml@asottile parameters: toxenvs: [py37] From 071cc422c772c8758feffe33afb7c5199b8c5990 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 31 May 2019 12:32:11 -0700 Subject: [PATCH 419/544] xfail default language version check for azure pipelines --- tests/repository_test.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/repository_test.py b/tests/repository_test.py index 97fcba052..03ffeb07c 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -4,6 +4,7 @@ import os.path import re import shutil +import sys import cfgv import mock @@ -717,6 +718,10 @@ def local_python_config(): return {'repo': 'local', 'hooks': hooks} +@pytest.mark.xfail( # pragma: windows no cover + sys.platform == 'win32', + reason='microsoft/azure-pipelines-image-generation#989', +) def test_local_python_repo(store, local_python_config): hook = _get_hook(local_python_config, store, 'foo') # language_version should have been adjusted to the interpreter version From 64f0178b75c7c2ae79d8a7b3962481721856fd71 Mon Sep 17 00:00:00 2001 From: Mandar Vaze Date: Sat, 1 Jun 2019 07:40:20 +0530 Subject: [PATCH 420/544] Pass color option to git diff unconditionally --- pre_commit/commands/run.py | 11 ++++------- tests/commands/run_test.py | 7 +------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 3c18dd569..a58e2747a 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -224,13 +224,10 @@ def _run_hooks(config, hooks, args, environ): '`pre-commit install`.', ) output.write_line('All changes made by hooks:') - if args.color: - subprocess.call(( - 'git', '--no-pager', 'diff', '--no-ext-diff', - '--color={}'.format(args.color), - )) - else: - subprocess.call(('git', '--no-pager', 'diff', '--no-ext-diff')) + subprocess.call(( + 'git', '--no-pager', 'diff', '--no-ext-diff', + '--color={}'.format(args.color), + )) return retval diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index b4548f6fb..a6266facc 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -181,12 +181,6 @@ def test_global_exclude(cap_out, store, tempdir_factory): @pytest.mark.parametrize( ('args', 'expected_out'), [ - ( - { - 'show_diff_on_failure': True, - }, - b'All changes made by hooks:', - ), ( { 'show_diff_on_failure': True, @@ -198,6 +192,7 @@ def test_global_exclude(cap_out, store, tempdir_factory): { 'show_diff_on_failure': True, 'all_files': True, + 'color': 'auto', }, b'reproduce locally with: pre-commit run --all-files', ), From 3d7b374bef1e102b4abe3ecbb7a09a5507e17939 Mon Sep 17 00:00:00 2001 From: Mandar Vaze Date: Sat, 1 Jun 2019 17:33:27 +0530 Subject: [PATCH 421/544] Pass correct value to git color based on args.color --- pre_commit/commands/run.py | 4 +++- tests/commands/run_test.py | 9 +++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index a58e2747a..33c0f10bc 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -224,9 +224,11 @@ def _run_hooks(config, hooks, args, environ): '`pre-commit install`.', ) output.write_line('All changes made by hooks:') + # args.color is a boolean. + # See user_color function in color.py subprocess.call(( 'git', '--no-pager', 'diff', '--no-ext-diff', - '--color={}'.format(args.color), + '--color={}'.format({True: 'always', False: 'never'}[args.color]), )) return retval diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index a6266facc..fc2a973c4 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -184,7 +184,13 @@ def test_global_exclude(cap_out, store, tempdir_factory): ( { 'show_diff_on_failure': True, - 'color': 'auto', + }, + b'All changes made by hooks:', + ), + ( + { + 'show_diff_on_failure': True, + 'color': True, }, b'All changes made by hooks:', ), @@ -192,7 +198,6 @@ def test_global_exclude(cap_out, store, tempdir_factory): { 'show_diff_on_failure': True, 'all_files': True, - 'color': 'auto', }, b'reproduce locally with: pre-commit run --all-files', ), From 016eda9f3c014b0777272f0f7119084f575e7c17 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 6 Jun 2019 08:30:11 -0700 Subject: [PATCH 422/544] v1.17.0 --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79629b797..fc1a2d30f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +1.17.0 - 2019-06-06 +=================== + +### Features +- Produce better output on `^C` + - #1030 PR by @asottile. +- Warn on unknown keys at the top level and repo level + - #1028 PR by @yoavcaspi. + - #1048 PR by @asottile. + +### Fixes +- Fix handling of `^C` in wrapper script in python 3.x + - #1027 PR by @asottile. +- Fix `rmtree` for non-writable directories + - #1042 issue by @detailyang. + - #1043 PR by @asottile. +- Pass `--color` option to `git diff` in `--show-diff-on-failure` + - #1007 issue by @chadrik. + - #1051 PR by @mandarvaze. + +### Misc. +- Fix test when `pre-commit` is installed globally + - #1032 issue by @yoavcaspi. + - #1045 PR by @asottile. + + 1.16.1 - 2019-05-08 =================== diff --git a/setup.cfg b/setup.cfg index eca74cc8d..3793677e9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.16.1 +version = 1.17.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 90128c5a9de18f747876faf0a8e6fae7bb15a7cc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 6 Jun 2019 08:41:09 -0700 Subject: [PATCH 423/544] Fixes for rust tests on azure pipelines linux --- azure-pipelines.yml | 4 +--- tox.ini | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 05ed0c3cd..381ff0e70 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -20,7 +20,7 @@ jobs: os: windows additional_variables: COVERAGE_IGNORE_WINDOWS: '# pragma: windows no cover' - TOX_TESTENV_PASSENV: COVERAGE_IGNORE_WINDOWS RUSTUP_HOME + TOX_TESTENV_PASSENV: COVERAGE_IGNORE_WINDOWS TEMP: C:\Temp # remove when dropping python2 - template: job--python-tox.yml@asottile parameters: @@ -30,7 +30,6 @@ jobs: pre_test: - task: UseRubyVersion@0 - template: step--git-install.yml - - template: step--rust-install.yml - bash: | testing/get-swift.sh echo '##vso[task.prependpath]/tmp/swift/usr/bin' @@ -41,7 +40,6 @@ jobs: os: linux pre_test: - task: UseRubyVersion@0 - - template: step--rust-install.yml - bash: | testing/get-swift.sh echo '##vso[task.prependpath]/tmp/swift/usr/bin' diff --git a/tox.ini b/tox.ini index a63b6533c..e24c24706 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist = py27,py36,py37,pypy,pypy3,pre-commit [testenv] deps = -rrequirements-dev.txt -passenv = HOME LOCALAPPDATA +passenv = HOME LOCALAPPDATA RUSTUP_HOME commands = coverage erase coverage run -m pytest {posargs:tests} From 9d1342aeb6f7bbc4cad7f55dfa4575e1532c5f22 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 9 Jun 2019 08:41:06 -0700 Subject: [PATCH 424/544] Document adding a supported language --- CONTRIBUTING.md | 103 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 98 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bb875ce75..cc206b522 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,8 +2,8 @@ ## Local development -- The complete test suite depends on having at least the following installed (possibly not - a complete list) +- The complete test suite depends on having at least the following installed + (possibly not a complete list) - git (A sufficiently newer version is required to run pre-push tests) - python2 (Required by a test which checks different python versions) - python3 (Required by a test which checks different python versions) @@ -30,7 +30,7 @@ Running a specific test with the environment activated is as easy as: ### Running all the tests -Running all the tests can be done by running `tox -e py27` (or your +Running all the tests can be done by running `tox -e py37` (or your interpreter version of choice). These often take a long time and consume significant cpu while running the slower node / ruby integration tests. @@ -49,5 +49,98 @@ Documentation is hosted at https://pre-commit.com This website is controlled through https://github.com/pre-commit/pre-commit.github.io -When adding a feature, please make a pull request to add yourself to the -contributors list and add documentation to the website if applicable. +## Adding support for a new hook language + +pre-commit already supports many [programming languages](https://pre-commit.com/#supported-languages) +to write hook executables with. + +When adding support for a language, you must first decide what level of support +to implement. The current implemented languages are at varying levels: + +- 0th class - pre-commit does not require any dependencies for these languages + as they're not actually languages (current examples: fail, pygrep) +- 1st class - pre-commit will bootstrap a full interpreter requiring nothing to + be installed globally (current examples: node, ruby) +- 2nd class - pre-commit requires the user to install the language globally but + will install tools in an isolated fashion (current examples: python, go, rust, + swift, docker). +- 3rd class - pre-commit requires the user to install both the tool and the + language globally (current examples: script, system) + +"third class" is usually the easiest to implement first and is perfectly +acceptable. + +Ideally the language works on the supported platforms for pre-commit (linux, +windows, macos) but it's ok to skip one or more platforms (for example, swift +doesn't run on windows). + +When writing your new language, it's often useful to look at other examples in +the `pre_commit/languages` directory. + +It might also be useful to look at a recent pull request which added a +language, for example: + +- [rust](https://github.com/pre-commit/pre-commit/pull/751) +- [fail](https://github.com/pre-commit/pre-commit/pull/812) +- [swift](https://github.com/pre-commit/pre-commit/pull/467) + +### `language` api + +here are the apis that should be implemented for a language + +Note that these are also documented in [`pre_commit/languages/all.py`](https://github.com/pre-commit/pre-commit/blob/master/pre_commit/languages/all.py) + +#### `ENVIRONMENT_DIR` + +a short string which will be used for the prefix of where packages will be +installed. For example, python uses `py_env` and installs a `virtualenv` at +that location. + +this will be `None` for 0th / 3rd class languages as they don't have an install +step. + +#### `get_default_version` + +This is used to retrieve the default `language_version` for a language. If +one cannot be determined, return `'default'`. + +You generally don't need to implement this on a first pass and can just use: + +```python +get_default_version = helpers.basic_default_version +``` + +`python` is currently the only language which implements this api + +#### `healthy` + +This is used to check whether the installed environment is considered healthy. +This function should return `True` or `False`. + +You generally don't need to implement this on a first pass and can just use: + +```python +healthy = helpers.basic_healthy +``` + +`python` is currently the only language which implements this api, for python +it is checking whether some common dlls are still available. + +#### `install_environment` + +this is the trickiest one to implement and where all the smart parts happen. + +this api should do the following things + +- (0th / 3rd class): `install_environment = helpers.no_install` +- (1st class): install a language runtime into the hook's directory +- (2nd class): install the package at `.` into the `ENVIRONMENT_DIR` +- (2nd class, optional): install packages listed in `additional_dependencies` + into `ENVIRONMENT_DIR` (not a required feature for a first pass) + +#### `run_hook` + +This is usually the easiest to implement, most of them look the same as the +`node` hook implementation: + +https://github.com/pre-commit/pre-commit/blob/160238220f022035c8ef869c9a8642f622c02118/pre_commit/languages/node.py#L72-L74 From 9bdce088c8304b23cd8ae161e99872db81065c02 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 17 Jun 2019 07:54:38 -0700 Subject: [PATCH 425/544] Use sys.executable if it matches on posix as well --- pre_commit/languages/python.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index ca1146700..5d48fb892 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -104,11 +104,11 @@ def _sys_executable_matches(version): def norm_version(version): - if os.name == 'nt': # pragma: no cover (windows) - # first see if our current executable is appropriate - if _sys_executable_matches(version): - return sys.executable + # first see if our current executable is appropriate + if _sys_executable_matches(version): + return sys.executable + if os.name == 'nt': # pragma: no cover (windows) version_exec = _find_by_py_launcher(version) if version_exec: return version_exec From f4b3add8ab91fe92bc3ca6c2f4edb7291bd603a4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 25 Jun 2019 18:19:10 -0700 Subject: [PATCH 426/544] Suggest tox --devenv instead of tox -e venv --- CONTRIBUTING.md | 4 ++-- azure-pipelines.yml | 2 +- tox.ini | 4 ---- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cc206b522..2b83c8232 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,8 +16,8 @@ This is useful for running specific tests. The easiest way to set this up is to run: -1. `tox -e venv` -2. `. venv-pre_commit/bin/activate` +1. `tox --devenv venv` (note: requires tox>=3.13) +2. `. venv/bin/activate` This will create and put you into a virtualenv which has an editable installation of pre-commit. Hack away! Running `pre-commit` will reflect diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 381ff0e70..30b873a0c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -10,7 +10,7 @@ resources: type: github endpoint: github name: asottile/azure-pipeline-templates - ref: refs/tags/v0.0.14 + ref: refs/tags/v0.0.15 jobs: - template: job--pre-commit.yml@asottile diff --git a/tox.ini b/tox.ini index e24c24706..1fac9332c 100644 --- a/tox.ini +++ b/tox.ini @@ -15,10 +15,6 @@ skip_install = true deps = pre-commit commands = pre-commit run --all-files --show-diff-on-failure -[testenv:venv] -envdir = venv-pre_commit -commands = - [pep8] ignore = E265,E501,W504 From b12e4e82aa65726b55f58522b37ea477a703d797 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 7 Jul 2019 09:41:28 -0700 Subject: [PATCH 427/544] MANIFEST.in is unnecessary with `license_file` --- MANIFEST.in | 1 - 1 file changed, 1 deletion(-) delete mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 1aba38f67..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -include LICENSE From 01653b80774688a8ae2acbf5b2c535eca4840273 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 7 Jul 2019 22:10:32 -0700 Subject: [PATCH 428/544] Fix shallow fetch by checking out FETCH_HEAD --- pre_commit/store.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index d1d432dc1..08733ab8a 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -142,15 +142,15 @@ def _complete_clone(self, ref, git_cmd): git_cmd('checkout', ref) git_cmd('submodule', 'update', '--init', '--recursive') - def _shallow_clone(self, ref, git_cmd): # pragma: windows no cover + def _shallow_clone(self, ref, git_cmd): """Perform a shallow clone of a repository and its submodules """ git_config = 'protocol.version=2' git_cmd('-c', git_config, 'fetch', 'origin', ref, '--depth=1') - git_cmd('checkout', ref) + git_cmd('checkout', 'FETCH_HEAD') git_cmd( - '-c', git_config, 'submodule', 'update', '--init', - '--recursive', '--depth=1', + '-c', git_config, 'submodule', 'update', '--init', '--recursive', + '--depth=1', ) def clone(self, repo, ref, deps=()): From c148845a984851973f7de535420b9645f0963e95 Mon Sep 17 00:00:00 2001 From: Michael Adkins Date: Tue, 9 Jul 2019 13:06:18 -0500 Subject: [PATCH 429/544] Added hook-stage print to output for missing hook id --- pre_commit/commands/run.py | 2 +- tests/commands/run_test.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 33c0f10bc..b858af4b3 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -285,7 +285,7 @@ def run(config_file, store, args, environ=os.environ): ] if args.hook and not hooks: - output.write_line('No hook with id `{}`'.format(args.hook)) + output.write_line('No hook with id `{}` in stage `{}`'.format(args.hook, args.hook_stage)) return 1 install_hook_envs(hooks, store) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index fc2a973c4..d29382342 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -231,7 +231,13 @@ def test_show_diff_on_failure( ({}, (b'Bash hook', b'Passed'), 0, True), ({'verbose': True}, (b'foo.py\nHello World',), 0, True), ({'hook': 'bash_hook'}, (b'Bash hook', b'Passed'), 0, True), - ({'hook': 'nope'}, (b'No hook with id `nope`',), 1, True), + ({'hook': 'nope'}, (b'No hook with id `nope` in stage `commit`',), 1, True), + ( + {'hook': 'nope', 'hook_stage': 'push'}, + (b'No hook with id `nope` in stage `push`',), + 1, + True + ), ( {'all_files': True, 'verbose': True}, (b'foo.py',), From 02d95c033cf2736b164412007695947644b83839 Mon Sep 17 00:00:00 2001 From: Michael Adkins Date: Tue, 9 Jul 2019 13:48:06 -0500 Subject: [PATCH 430/544] Fixed code style --- pre_commit/commands/run.py | 6 +++++- tests/commands/run_test.py | 9 +++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index b858af4b3..4087a6505 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -285,7 +285,11 @@ def run(config_file, store, args, environ=os.environ): ] if args.hook and not hooks: - output.write_line('No hook with id `{}` in stage `{}`'.format(args.hook, args.hook_stage)) + output.write_line( + 'No hook with id `{}` in stage `{}`'.format( + args.hook, args.hook_stage, + ), + ) return 1 install_hook_envs(hooks, store) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index d29382342..94d44e150 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -231,12 +231,17 @@ def test_show_diff_on_failure( ({}, (b'Bash hook', b'Passed'), 0, True), ({'verbose': True}, (b'foo.py\nHello World',), 0, True), ({'hook': 'bash_hook'}, (b'Bash hook', b'Passed'), 0, True), - ({'hook': 'nope'}, (b'No hook with id `nope` in stage `commit`',), 1, True), + ( + {'hook': 'nope'}, + (b'No hook with id `nope` in stage `commit`',), + 1, + True, + ), ( {'hook': 'nope', 'hook_stage': 'push'}, (b'No hook with id `nope` in stage `push`',), 1, - True + True, ), ( {'all_files': True, 'verbose': True}, From 73250ff4e32414e2c8fe8c7226aa92591a4001f7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 20 Jul 2019 14:19:46 -0700 Subject: [PATCH 431/544] Fix autoupdate to always use non-shallow clone --- pre_commit/commands/autoupdate.py | 29 ++++++++++++++++------------- pre_commit/git.py | 9 +++++++++ pre_commit/store.py | 10 ++-------- tests/commands/gc_test.py | 4 +++- 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 11712e17d..9701e9376 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -10,6 +10,7 @@ from cfgv import remove_defaults import pre_commit.constants as C +from pre_commit import git from pre_commit import output from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import InvalidManifestError @@ -20,6 +21,7 @@ from pre_commit.commands.migrate_config import migrate_config from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output +from pre_commit.util import tmpdir class RepositoryCannotBeUpdatedError(RuntimeError): @@ -34,19 +36,20 @@ def _update_repo(repo_config, store, tags_only): Args: repo_config - A config for a repository """ - repo_path = store.clone(repo_config['repo'], repo_config['rev']) - - cmd_output('git', 'fetch', cwd=repo_path) - tag_cmd = ('git', 'describe', 'origin/master', '--tags') - if tags_only: - tag_cmd += ('--abbrev=0',) - else: - tag_cmd += ('--exact',) - try: - rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip() - except CalledProcessError: - tag_cmd = ('git', 'rev-parse', 'origin/master') - rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip() + with tmpdir() as repo_path: + git.init_repo(repo_path, repo_config['repo']) + cmd_output('git', 'fetch', cwd=repo_path) + + tag_cmd = ('git', 'describe', 'origin/master', '--tags') + if tags_only: + tag_cmd += ('--abbrev=0',) + else: + tag_cmd += ('--exact',) + try: + rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip() + except CalledProcessError: + tag_cmd = ('git', 'rev-parse', 'origin/master') + rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip() # Don't bother trying to update if our rev is the same if rev == repo_config['rev']: diff --git a/pre_commit/git.py b/pre_commit/git.py index 64e449cbf..c51930e7d 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -143,6 +143,15 @@ def has_diff(*args, **kwargs): return cmd_output(*cmd, cwd=repo, retcode=None)[0] +def init_repo(path, remote): + if os.path.isdir(remote): + remote = os.path.abspath(remote) + + env = no_git_env() + cmd_output('git', 'init', path, env=env) + cmd_output('git', 'remote', 'add', 'origin', remote, cwd=path, env=env) + + def commit(repo='.'): env = no_git_env() name, email = 'pre-commit', 'asottile+pre-commit@umich.edu' diff --git a/pre_commit/store.py b/pre_commit/store.py index 08733ab8a..55c57a3e6 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -156,18 +156,13 @@ def _shallow_clone(self, ref, git_cmd): def clone(self, repo, ref, deps=()): """Clone the given url and checkout the specific ref.""" - if os.path.isdir(repo): - repo = os.path.abspath(repo) - def clone_strategy(directory): + git.init_repo(directory, repo) env = git.no_git_env() def _git_cmd(*args): cmd_output('git', *args, cwd=directory, env=env) - _git_cmd('init', '.') - _git_cmd('remote', 'add', 'origin', repo) - try: self._shallow_clone(ref, _git_cmd) except CalledProcessError: @@ -193,8 +188,7 @@ def make_local_strategy(directory): def _git_cmd(*args): cmd_output('git', *args, cwd=directory, env=env) - _git_cmd('init', '.') - _git_cmd('config', 'remote.origin.url', '<>') + git.init_repo(directory, '<>') _git_cmd('add', '.') git.commit(repo=directory) diff --git a/tests/commands/gc_test.py b/tests/commands/gc_test.py index d2528507e..5be86b1b4 100644 --- a/tests/commands/gc_test.py +++ b/tests/commands/gc_test.py @@ -5,6 +5,7 @@ from pre_commit.clientlib import load_config from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.gc import gc +from pre_commit.commands.install_uninstall import install_hooks from pre_commit.repository import all_hooks from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo @@ -40,6 +41,7 @@ def test_gc(tempdir_factory, store, in_git_dir, cap_out): store.mark_config_used(C.CONFIG_FILE) # update will clone both the old and new repo, making the old one gc-able + install_hooks(C.CONFIG_FILE, store) assert not autoupdate(C.CONFIG_FILE, store, tags_only=False) assert _config_count(store) == 1 @@ -145,7 +147,7 @@ def test_invalid_manifest_gcd(tempdir_factory, store, in_git_dir, cap_out): store.mark_config_used(C.CONFIG_FILE) # trigger a clone - assert not autoupdate(C.CONFIG_FILE, store, tags_only=False) + install_hooks(C.CONFIG_FILE, store) # we'll "break" the manifest to simulate an old version clone (_, _, path), = store.select_all_repos() From 8be0f857e8a9faf7d8c84f314e0c4e991353878b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 20 Jul 2019 14:52:28 -0700 Subject: [PATCH 432/544] Make autoupdate work for non-master default branches --- pre_commit/commands/autoupdate.py | 6 +++--- tests/commands/autoupdate_test.py | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 9701e9376..fdada1858 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -38,9 +38,9 @@ def _update_repo(repo_config, store, tags_only): """ with tmpdir() as repo_path: git.init_repo(repo_path, repo_config['repo']) - cmd_output('git', 'fetch', cwd=repo_path) + cmd_output('git', 'fetch', 'origin', 'HEAD', '--tags', cwd=repo_path) - tag_cmd = ('git', 'describe', 'origin/master', '--tags') + tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags') if tags_only: tag_cmd += ('--abbrev=0',) else: @@ -48,7 +48,7 @@ def _update_repo(repo_config, store, tags_only): try: rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip() except CalledProcessError: - tag_cmd = ('git', 'rev-parse', 'origin/master') + tag_cmd = ('git', 'rev-parse', 'FETCH_HEAD') rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip() # Don't bother trying to update if our rev is the same diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index c1fceb42e..ead0efe57 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -309,6 +309,12 @@ def test_autoupdate_hook_disappearing_repo( assert before == after +def test_autoupdate_non_master_default_branch(up_to_date_repo, store): + # change the default branch to be not-master + cmd_output('git', '-C', up_to_date_repo, 'branch', '-m', 'dev') + test_up_to_date_repo(up_to_date_repo, store) + + def test_autoupdate_local_hooks(in_git_dir, store): config = sample_local_config() add_config_to_repo('.', config) From 3def940574f1812d1d5627ec0d7deeb07fb21a27 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 20 Jul 2019 16:24:10 -0700 Subject: [PATCH 433/544] reorder pre-commit sub commands --- pre_commit/main.py | 106 ++++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index aa7ff2a79..d5c488f87 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -131,6 +131,37 @@ def main(argv=None): subparsers = parser.add_subparsers(dest='command') + autoupdate_parser = subparsers.add_parser( + 'autoupdate', + help="Auto-update pre-commit config to the latest repos' versions.", + ) + _add_color_option(autoupdate_parser) + _add_config_option(autoupdate_parser) + autoupdate_parser.add_argument( + '--tags-only', action='store_true', help='LEGACY: for compatibility', + ) + autoupdate_parser.add_argument( + '--bleeding-edge', action='store_true', + help=( + 'Update to the bleeding edge of `master` instead of the latest ' + 'tagged version (the default behavior).' + ), + ) + autoupdate_parser.add_argument( + '--repo', dest='repos', action='append', metavar='REPO', + help='Only update this repository -- may be specified multiple times.', + ) + + clean_parser = subparsers.add_parser( + 'clean', help='Clean out pre-commit files.', + ) + _add_color_option(clean_parser) + _add_config_option(clean_parser) + + gc_parser = subparsers.add_parser('gc', help='Clean unused cached repos.') + _add_color_option(gc_parser) + _add_config_option(gc_parser) + install_parser = subparsers.add_parser( 'install', help='Install the pre-commit script.', ) @@ -167,44 +198,6 @@ def main(argv=None): _add_color_option(install_hooks_parser) _add_config_option(install_hooks_parser) - uninstall_parser = subparsers.add_parser( - 'uninstall', help='Uninstall the pre-commit script.', - ) - _add_color_option(uninstall_parser) - _add_config_option(uninstall_parser) - _add_hook_type_option(uninstall_parser) - - clean_parser = subparsers.add_parser( - 'clean', help='Clean out pre-commit files.', - ) - _add_color_option(clean_parser) - _add_config_option(clean_parser) - - gc_parser = subparsers.add_parser('gc', help='Clean unused cached repos.') - _add_color_option(gc_parser) - _add_config_option(gc_parser) - - autoupdate_parser = subparsers.add_parser( - 'autoupdate', - help="Auto-update pre-commit config to the latest repos' versions.", - ) - _add_color_option(autoupdate_parser) - _add_config_option(autoupdate_parser) - autoupdate_parser.add_argument( - '--tags-only', action='store_true', help='LEGACY: for compatibility', - ) - autoupdate_parser.add_argument( - '--bleeding-edge', action='store_true', - help=( - 'Update to the bleeding edge of `master` instead of the latest ' - 'tagged version (the default behavior).' - ), - ) - autoupdate_parser.add_argument( - '--repo', dest='repos', action='append', metavar='REPO', - help='Only update this repository -- may be specified multiple times.', - ) - migrate_config_parser = subparsers.add_parser( 'migrate-config', help='Migrate list configuration to new map configuration.', @@ -241,6 +234,13 @@ def main(argv=None): ) _add_run_options(try_repo_parser) + uninstall_parser = subparsers.add_parser( + 'uninstall', help='Uninstall the pre-commit script.', + ) + _add_color_option(uninstall_parser) + _add_config_option(uninstall_parser) + _add_hook_type_option(uninstall_parser) + help = subparsers.add_parser( 'help', help='Show help for a specific command.', ) @@ -265,29 +265,27 @@ def main(argv=None): store = Store() store.mark_config_used(args.config) - if args.command == 'install': - return install( + if args.command == 'autoupdate': + if args.tags_only: + logger.warning('--tags-only is the default') + return autoupdate( args.config, store, - overwrite=args.overwrite, hooks=args.install_hooks, - hook_type=args.hook_type, - skip_on_missing_conf=args.allow_missing_config, + tags_only=not args.bleeding_edge, + repos=args.repos, ) - elif args.command == 'install-hooks': - return install_hooks(args.config, store) - elif args.command == 'uninstall': - return uninstall(hook_type=args.hook_type) elif args.command == 'clean': return clean(store) elif args.command == 'gc': return gc(store) - elif args.command == 'autoupdate': - if args.tags_only: - logger.warning('--tags-only is the default') - return autoupdate( + elif args.command == 'install': + return install( args.config, store, - tags_only=not args.bleeding_edge, - repos=args.repos, + overwrite=args.overwrite, hooks=args.install_hooks, + hook_type=args.hook_type, + skip_on_missing_conf=args.allow_missing_config, ) + elif args.command == 'install-hooks': + return install_hooks(args.config, store) elif args.command == 'migrate-config': return migrate_config(args.config) elif args.command == 'run': @@ -296,6 +294,8 @@ def main(argv=None): return sample_config() elif args.command == 'try-repo': return try_repo(args) + elif args.command == 'uninstall': + return uninstall(hook_type=args.hook_type) else: raise NotImplementedError( 'Command {} not implemented.'.format(args.command), From 9a52eefc99d3d9a392110249a6b938b510a66410 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 20 Jul 2019 19:10:50 -0700 Subject: [PATCH 434/544] Implement `pre-commit init-templatedir` --- pre_commit/commands/init_templatedir.py | 21 ++++++++++ pre_commit/commands/install_uninstall.py | 11 +++--- pre_commit/main.py | 22 ++++++++++- tests/commands/init_templatedir_test.py | 49 ++++++++++++++++++++++++ tests/commands/install_uninstall_test.py | 6 +-- tests/conftest.py | 8 ++++ tests/main_test.py | 6 +++ 7 files changed, 114 insertions(+), 9 deletions(-) create mode 100644 pre_commit/commands/init_templatedir.py create mode 100644 tests/commands/init_templatedir_test.py diff --git a/pre_commit/commands/init_templatedir.py b/pre_commit/commands/init_templatedir.py new file mode 100644 index 000000000..c1b95621f --- /dev/null +++ b/pre_commit/commands/init_templatedir.py @@ -0,0 +1,21 @@ +import logging +import os.path + +from pre_commit.commands.install_uninstall import install +from pre_commit.util import cmd_output + +logger = logging.getLogger('pre_commit') + + +def init_templatedir(config_file, store, directory, hook_type): + install( + config_file, store, overwrite=True, hook_type=hook_type, + skip_on_missing_config=True, git_dir=directory, + ) + _, out, _ = cmd_output('git', 'config', 'init.templateDir', retcode=None) + dest = os.path.realpath(directory) + if os.path.realpath(out.strip()) != dest: + logger.warning('`init.templateDir` not set to the target directory') + logger.warning( + 'maybe `git config --global init.templateDir {}`?'.format(dest), + ) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 701afccb3..9b2c3b807 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -34,8 +34,9 @@ TEMPLATE_END = '# end templated\n' -def _hook_paths(hook_type): - pth = os.path.join(git.get_git_dir(), 'hooks', hook_type) +def _hook_paths(hook_type, git_dir=None): + git_dir = git_dir if git_dir is not None else git.get_git_dir() + pth = os.path.join(git_dir, 'hooks', hook_type) return pth, '{}.legacy'.format(pth) @@ -69,7 +70,7 @@ def shebang(): def install( config_file, store, overwrite=False, hooks=False, hook_type='pre-commit', - skip_on_missing_conf=False, + skip_on_missing_config=False, git_dir=None, ): """Install the pre-commit hooks.""" if cmd_output('git', 'config', 'core.hooksPath', retcode=None)[1].strip(): @@ -79,7 +80,7 @@ def install( ) return 1 - hook_path, legacy_path = _hook_paths(hook_type) + hook_path, legacy_path = _hook_paths(hook_type, git_dir=git_dir) mkdirp(os.path.dirname(hook_path)) @@ -100,7 +101,7 @@ def install( 'CONFIG': config_file, 'HOOK_TYPE': hook_type, 'INSTALL_PYTHON': sys.executable, - 'SKIP_ON_MISSING_CONFIG': skip_on_missing_conf, + 'SKIP_ON_MISSING_CONFIG': skip_on_missing_config, } with io.open(hook_path, 'w') as hook_file: diff --git a/pre_commit/main.py b/pre_commit/main.py index d5c488f87..67a67a05c 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -12,6 +12,7 @@ from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.clean import clean from pre_commit.commands.gc import gc +from pre_commit.commands.init_templatedir import init_templatedir from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import install_hooks from pre_commit.commands.install_uninstall import uninstall @@ -162,6 +163,20 @@ def main(argv=None): _add_color_option(gc_parser) _add_config_option(gc_parser) + init_templatedir_parser = subparsers.add_parser( + 'init-templatedir', + help=( + 'Install hook script in a directory intended for use with ' + '`git config init.templateDir`.' + ), + ) + _add_color_option(init_templatedir_parser) + _add_config_option(init_templatedir_parser) + init_templatedir_parser.add_argument( + 'directory', help='The directory in which to write the hook script.', + ) + _add_hook_type_option(init_templatedir_parser) + install_parser = subparsers.add_parser( 'install', help='Install the pre-commit script.', ) @@ -282,7 +297,12 @@ def main(argv=None): args.config, store, overwrite=args.overwrite, hooks=args.install_hooks, hook_type=args.hook_type, - skip_on_missing_conf=args.allow_missing_config, + skip_on_missing_config=args.allow_missing_config, + ) + elif args.command == 'init-templatedir': + return init_templatedir( + args.config, store, + args.directory, hook_type=args.hook_type, ) elif args.command == 'install-hooks': return install_hooks(args.config, store) diff --git a/tests/commands/init_templatedir_test.py b/tests/commands/init_templatedir_test.py new file mode 100644 index 000000000..2910ac9e2 --- /dev/null +++ b/tests/commands/init_templatedir_test.py @@ -0,0 +1,49 @@ +import subprocess + +import pre_commit.constants as C +from pre_commit.commands.init_templatedir import init_templatedir +from pre_commit.envcontext import envcontext +from pre_commit.util import cmd_output +from testing.fixtures import git_dir +from testing.fixtures import make_consuming_repo +from testing.util import cmd_output_mocked_pre_commit_home +from testing.util import cwd +from testing.util import git_commit + + +def test_init_templatedir(tmpdir, tempdir_factory, store, cap_out): + target = str(tmpdir.join('tmpl')) + init_templatedir(C.CONFIG_FILE, store, target, hook_type='pre-commit') + lines = cap_out.get().splitlines() + assert lines[0].startswith('pre-commit installed at ') + assert lines[1] == ( + '[WARNING] `init.templateDir` not set to the target directory' + ) + assert lines[2].startswith( + '[WARNING] maybe `git config --global init.templateDir', + ) + + with envcontext([('GIT_TEMPLATE_DIR', target)]): + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + + with cwd(path): + retcode, output, _ = git_commit( + fn=cmd_output_mocked_pre_commit_home, + tempdir_factory=tempdir_factory, + # git commit puts pre-commit to stderr + stderr=subprocess.STDOUT, + ) + assert retcode == 0 + assert 'Bash hook....' in output + + +def test_init_templatedir_already_set(tmpdir, tempdir_factory, store, cap_out): + target = str(tmpdir.join('tmpl')) + tmp_git_dir = git_dir(tempdir_factory) + with cwd(tmp_git_dir): + cmd_output('git', 'config', 'init.templateDir', target) + init_templatedir(C.CONFIG_FILE, store, target, hook_type='pre-commit') + + lines = cap_out.get().splitlines() + assert len(lines) == 1 + assert lines[0].startswith('pre-commit installed at') diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 5fdf94991..913bf74eb 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -735,7 +735,7 @@ def test_install_disallow_missing_config(tempdir_factory, store): with cwd(path): remove_config_from_repo(path) ret = install( - C.CONFIG_FILE, store, overwrite=True, skip_on_missing_conf=False, + C.CONFIG_FILE, store, overwrite=True, skip_on_missing_config=False, ) assert ret == 0 @@ -748,7 +748,7 @@ def test_install_allow_missing_config(tempdir_factory, store): with cwd(path): remove_config_from_repo(path) ret = install( - C.CONFIG_FILE, store, overwrite=True, skip_on_missing_conf=True, + C.CONFIG_FILE, store, overwrite=True, skip_on_missing_config=True, ) assert ret == 0 @@ -766,7 +766,7 @@ def test_install_temporarily_allow_mising_config(tempdir_factory, store): with cwd(path): remove_config_from_repo(path) ret = install( - C.CONFIG_FILE, store, overwrite=True, skip_on_missing_conf=False, + C.CONFIG_FILE, store, overwrite=True, skip_on_missing_config=False, ) assert ret == 0 diff --git a/tests/conftest.py b/tests/conftest.py index 23ff7460a..635ea39af 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,6 +11,7 @@ import six from pre_commit import output +from pre_commit.envcontext import envcontext from pre_commit.logging_handler import logging_handler from pre_commit.store import Store from pre_commit.util import cmd_output @@ -272,3 +273,10 @@ def fake_log_handler(): logger.addHandler(handler) yield handler logger.removeHandler(handler) + + +@pytest.fixture(scope='session', autouse=True) +def set_git_templatedir(tmpdir_factory): + tdir = str(tmpdir_factory.mktemp('git_template_dir')) + with envcontext([('GIT_TEMPLATE_DIR', tdir)]): + yield diff --git a/tests/main_test.py b/tests/main_test.py index e5573b88d..75fd56001 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -140,6 +140,12 @@ def test_try_repo(mock_store_dir): assert patch.call_count == 1 +def test_init_templatedir(mock_store_dir): + with mock.patch.object(main, 'init_templatedir') as patch: + main.main(('init-templatedir', 'tdir')) + assert patch.call_count == 1 + + def test_help_cmd_in_empty_directory( in_tmpdir, mock_commands, From 1bf9ff74939d899fe18a2325a29b7a59f953d214 Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Mon, 22 Jul 2019 19:18:36 +0200 Subject: [PATCH 435/544] Don't use color if NO_COLOR environment variable is set --- pre_commit/color.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pre_commit/color.py b/pre_commit/color.py index c785e2c9f..831d50bf9 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -48,6 +48,9 @@ def use_color(setting): if setting not in COLOR_CHOICES: raise InvalidColorSetting(setting) + if 'NO_COLOR' in os.environ: + return False + return ( setting == 'always' or (setting == 'auto' and sys.stdout.isatty() and terminal_supports_color) From 01d3a72a0ed1e3a43a45b9908a5b9200593dee32 Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Mon, 22 Jul 2019 19:35:39 +0200 Subject: [PATCH 436/544] Require NO_COLOR environment variable to be non-empty to disable colors --- pre_commit/color.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/color.py b/pre_commit/color.py index 831d50bf9..102639adc 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -48,7 +48,7 @@ def use_color(setting): if setting not in COLOR_CHOICES: raise InvalidColorSetting(setting) - if 'NO_COLOR' in os.environ: + if 'NO_COLOR' in os.environ and os.environ['NO_COLOR']: return False return ( From 85204550425b69990c0c3b28e66296b013901a33 Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Mon, 22 Jul 2019 20:07:16 +0200 Subject: [PATCH 437/544] Add tests for NO_COLOR support --- tests/color_test.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/color_test.py b/tests/color_test.py index 6e11765ce..fb311c856 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import os import sys import mock @@ -50,3 +51,20 @@ def test_use_color_tty_without_color_support(): def test_use_color_raises_if_given_shenanigans(): with pytest.raises(InvalidColorSetting): use_color('herpaderp') + + +def test_no_color_env_unset(): + with mock.patch.dict(os.environ): + if 'NO_COLOR' in os.environ: + del os.environ['NO_COLOR'] + assert use_color('always') is True + + +def test_no_color_env_empty(): + with mock.patch.dict(os.environ, NO_COLOR=''): + assert use_color('always') is True + + +def test_no_color_env_non_empty(): + with mock.patch.dict(os.environ, NO_COLOR=' '): + assert use_color('always') is False From e9ff1be96c831a1f77708b782dca19fe6d7250da Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Mon, 22 Jul 2019 20:09:32 +0200 Subject: [PATCH 438/544] Simplify NO_COLOR check --- pre_commit/color.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/color.py b/pre_commit/color.py index 102639adc..2ede410a5 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -48,7 +48,7 @@ def use_color(setting): if setting not in COLOR_CHOICES: raise InvalidColorSetting(setting) - if 'NO_COLOR' in os.environ and os.environ['NO_COLOR']: + if os.environ.get('NO_COLOR'): return False return ( From 84dcb911196783cde209b095acc26d4089a24200 Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Mon, 22 Jul 2019 20:23:59 +0200 Subject: [PATCH 439/544] Change test to remove missed branch --- tests/color_test.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/color_test.py b/tests/color_test.py index fb311c856..4ba3f3272 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -54,9 +54,7 @@ def test_use_color_raises_if_given_shenanigans(): def test_no_color_env_unset(): - with mock.patch.dict(os.environ): - if 'NO_COLOR' in os.environ: - del os.environ['NO_COLOR'] + with mock.patch.dict(os.environ, clear=True): assert use_color('always') is True From b7ce5db782c0965aae064f7040177a702ca5e930 Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Tue, 23 Jul 2019 12:38:09 +0200 Subject: [PATCH 440/544] Use fallback uid and gid if os.getuid() and os.getgid() are unavailable --- pre_commit/languages/docker.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 59a53b4fb..8eaf6f4ec 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -14,6 +14,8 @@ ENVIRONMENT_DIR = 'docker' PRE_COMMIT_LABEL = 'PRE_COMMIT' +FALLBACK_UID = 1000 +FALLBACK_GID = 1000 get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy @@ -73,11 +75,25 @@ def install_environment( os.mkdir(directory) +def getuid(): + try: + return os.getuid() + except AttributeError: + return FALLBACK_UID + + +def getgid(): + try: + return os.getgid() + except AttributeError: + return FALLBACK_GID + + def docker_cmd(): # pragma: windows no cover return ( 'docker', 'run', '--rm', - '-u', '{}:{}'.format(os.getuid(), os.getgid()), + '-u', '{}:{}'.format(getuid(), getgid()), # https://docs.docker.com/engine/reference/commandline/run/#mount-volumes-from-container-volumes-from # The `Z` option tells Docker to label the content with a private # unshared label. Only the current container can use a private volume. From b43b6a61ab89cae3c8fdd011c79cbe6d8716d7bb Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Tue, 23 Jul 2019 15:14:06 +0200 Subject: [PATCH 441/544] Add docker uid and gid fallback tests --- pre_commit/languages/docker.py | 4 ++-- tests/languages/docker_test.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 8eaf6f4ec..8f7c72df9 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -75,14 +75,14 @@ def install_environment( os.mkdir(directory) -def getuid(): +def getuid(): # pragma: windows no cover try: return os.getuid() except AttributeError: return FALLBACK_UID -def getgid(): +def getgid(): # pragma: windows no cover try: return os.getgid() except AttributeError: diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 9f7f55cf2..43b7d1ce5 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -13,3 +13,17 @@ def test_docker_is_running_process_error(): side_effect=CalledProcessError(*(None,) * 4), ): assert docker.docker_is_running() is False + + +def test_docker_fallback_uid(): + def invalid_attribute(): + raise AttributeError + with mock.patch('os.getuid', invalid_attribute): + assert docker.getuid() == docker.FALLBACK_UID + + +def test_docker_fallback_gid(): + def invalid_attribute(): + raise AttributeError + with mock.patch('os.getgid', invalid_attribute): + assert docker.getgid() == docker.FALLBACK_GID From a21a4f46c79f6531f2a305f58dacce12f46d27fb Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Tue, 23 Jul 2019 15:35:19 +0200 Subject: [PATCH 442/544] Fix missing create=True attribute in docker tests --- tests/languages/docker_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 43b7d1ce5..a4cfbac1e 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -18,12 +18,12 @@ def test_docker_is_running_process_error(): def test_docker_fallback_uid(): def invalid_attribute(): raise AttributeError - with mock.patch('os.getuid', invalid_attribute): + with mock.patch('os.getuid', invalid_attribute, create=True): assert docker.getuid() == docker.FALLBACK_UID def test_docker_fallback_gid(): def invalid_attribute(): raise AttributeError - with mock.patch('os.getgid', invalid_attribute): + with mock.patch('os.getgid', invalid_attribute, create=True): assert docker.getgid() == docker.FALLBACK_GID From 07797f3fff7090d091b9fb64fff4358f81487190 Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Tue, 23 Jul 2019 17:37:44 +0200 Subject: [PATCH 443/544] Revert "Change test to remove missed branch" This reverts commit 84dcb911196783cde209b095acc26d4089a24200. --- tests/color_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/color_test.py b/tests/color_test.py index 4ba3f3272..fb311c856 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -54,7 +54,9 @@ def test_use_color_raises_if_given_shenanigans(): def test_no_color_env_unset(): - with mock.patch.dict(os.environ, clear=True): + with mock.patch.dict(os.environ): + if 'NO_COLOR' in os.environ: + del os.environ['NO_COLOR'] assert use_color('always') is True From 69b2cb5ea67eec5f171490c7a5f4aa568718e39c Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Tue, 23 Jul 2019 17:38:29 +0200 Subject: [PATCH 444/544] Revert "Simplify NO_COLOR check" This reverts commit e9ff1be96c831a1f77708b782dca19fe6d7250da. --- pre_commit/color.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/color.py b/pre_commit/color.py index 2ede410a5..102639adc 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -48,7 +48,7 @@ def use_color(setting): if setting not in COLOR_CHOICES: raise InvalidColorSetting(setting) - if os.environ.get('NO_COLOR'): + if 'NO_COLOR' in os.environ and os.environ['NO_COLOR']: return False return ( From e82c1e7259d646793368746dcfbf6e8d23408b0c Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Tue, 23 Jul 2019 17:38:50 +0200 Subject: [PATCH 445/544] Revert "Add tests for NO_COLOR support" This reverts commit 85204550425b69990c0c3b28e66296b013901a33. --- tests/color_test.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/tests/color_test.py b/tests/color_test.py index fb311c856..6e11765ce 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -import os import sys import mock @@ -51,20 +50,3 @@ def test_use_color_tty_without_color_support(): def test_use_color_raises_if_given_shenanigans(): with pytest.raises(InvalidColorSetting): use_color('herpaderp') - - -def test_no_color_env_unset(): - with mock.patch.dict(os.environ): - if 'NO_COLOR' in os.environ: - del os.environ['NO_COLOR'] - assert use_color('always') is True - - -def test_no_color_env_empty(): - with mock.patch.dict(os.environ, NO_COLOR=''): - assert use_color('always') is True - - -def test_no_color_env_non_empty(): - with mock.patch.dict(os.environ, NO_COLOR=' '): - assert use_color('always') is False From df919e6ab52d9bbcecba98e86ded5d9d722d3cab Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Tue, 23 Jul 2019 17:39:34 +0200 Subject: [PATCH 446/544] Revert "Require NO_COLOR environment variable to be non-empty to disable colors" This reverts commit 01d3a72a0ed1e3a43a45b9908a5b9200593dee32. --- pre_commit/color.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/color.py b/pre_commit/color.py index 102639adc..831d50bf9 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -48,7 +48,7 @@ def use_color(setting): if setting not in COLOR_CHOICES: raise InvalidColorSetting(setting) - if 'NO_COLOR' in os.environ and os.environ['NO_COLOR']: + if 'NO_COLOR' in os.environ: return False return ( From c75d8939f892b7806c33e96f8c2c2ff8cafd04ff Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Tue, 23 Jul 2019 17:40:08 +0200 Subject: [PATCH 447/544] Revert "Don't use color if NO_COLOR environment variable is set" This reverts commit 1bf9ff74939d899fe18a2325a29b7a59f953d214. --- pre_commit/color.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pre_commit/color.py b/pre_commit/color.py index 831d50bf9..c785e2c9f 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -48,9 +48,6 @@ def use_color(setting): if setting not in COLOR_CHOICES: raise InvalidColorSetting(setting) - if 'NO_COLOR' in os.environ: - return False - return ( setting == 'always' or (setting == 'auto' and sys.stdout.isatty() and terminal_supports_color) From aaa249bda9403dc2699eae0d73e64a16bf02ad65 Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Tue, 23 Jul 2019 17:42:28 +0200 Subject: [PATCH 448/544] Overwrite default value of --color argument with PRE_COMMIT_COLOR env var --- pre_commit/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 67a67a05c..53c2dba56 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -38,7 +38,8 @@ def _add_color_option(parser): parser.add_argument( - '--color', default='auto', type=color.use_color, + '--color', default=os.environ.get('PRE_COMMIT_COLOR', 'auto'), + type=color.use_color, metavar='{' + ','.join(color.COLOR_CHOICES) + '}', help='Whether to use color in output. Defaults to `%(default)s`.', ) From d4a9ff4d1f044d17040fa0b4b93ea93a8da4888e Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Thu, 25 Jul 2019 11:20:03 +0200 Subject: [PATCH 449/544] Simplify docker user fallback implementation and test --- pre_commit/languages/docker.py | 17 ++++------------- tests/languages/docker_test.py | 17 +++++++---------- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 8f7c72df9..4517050be 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -14,8 +14,6 @@ ENVIRONMENT_DIR = 'docker' PRE_COMMIT_LABEL = 'PRE_COMMIT' -FALLBACK_UID = 1000 -FALLBACK_GID = 1000 get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy @@ -75,25 +73,18 @@ def install_environment( os.mkdir(directory) -def getuid(): # pragma: windows no cover +def get_docker_user(): # pragma: windows no cover try: - return os.getuid() + return '{}:{}'.format(os.getuid(), os.getgid()) except AttributeError: - return FALLBACK_UID - - -def getgid(): # pragma: windows no cover - try: - return os.getgid() - except AttributeError: - return FALLBACK_GID + return '1000:1000' def docker_cmd(): # pragma: windows no cover return ( 'docker', 'run', '--rm', - '-u', '{}:{}'.format(getuid(), getgid()), + '-u', get_docker_user(), # https://docs.docker.com/engine/reference/commandline/run/#mount-volumes-from-container-volumes-from # The `Z` option tells Docker to label the content with a private # unshared label. Only the current container can use a private volume. diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index a4cfbac1e..1a96e69dc 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -15,15 +15,12 @@ def test_docker_is_running_process_error(): assert docker.docker_is_running() is False -def test_docker_fallback_uid(): +def test_docker_fallback_user(): def invalid_attribute(): raise AttributeError - with mock.patch('os.getuid', invalid_attribute, create=True): - assert docker.getuid() == docker.FALLBACK_UID - - -def test_docker_fallback_gid(): - def invalid_attribute(): - raise AttributeError - with mock.patch('os.getgid', invalid_attribute, create=True): - assert docker.getgid() == docker.FALLBACK_GID + with mock.patch.multiple( + 'os', create=True, + getuid=invalid_attribute, + getgid=invalid_attribute, + ): + assert docker.get_docker_user() == '1000:1000' From 120cae9d41b64fe12cbd0d064200750985f2f2d4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 27 Jul 2019 13:46:30 -0700 Subject: [PATCH 450/544] Disable color if TERM=dumb is detected --- pre_commit/color.py | 8 ++++++-- tests/color_test.py | 14 ++++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/pre_commit/color.py b/pre_commit/color.py index c785e2c9f..1fb6acceb 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -49,6 +49,10 @@ def use_color(setting): raise InvalidColorSetting(setting) return ( - setting == 'always' or - (setting == 'auto' and sys.stdout.isatty() and terminal_supports_color) + setting == 'always' or ( + setting == 'auto' and + sys.stdout.isatty() and + terminal_supports_color and + os.getenv('TERM') != 'dumb' + ) ) diff --git a/tests/color_test.py b/tests/color_test.py index 6e11765ce..6c9889d1b 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -5,6 +5,7 @@ import mock import pytest +from pre_commit import envcontext from pre_commit.color import format_color from pre_commit.color import GREEN from pre_commit.color import InvalidColorSetting @@ -38,13 +39,22 @@ def test_use_color_no_tty(): def test_use_color_tty_with_color_support(): with mock.patch.object(sys.stdout, 'isatty', return_value=True): with mock.patch('pre_commit.color.terminal_supports_color', True): - assert use_color('auto') is True + with envcontext.envcontext([('TERM', envcontext.UNSET)]): + assert use_color('auto') is True def test_use_color_tty_without_color_support(): with mock.patch.object(sys.stdout, 'isatty', return_value=True): with mock.patch('pre_commit.color.terminal_supports_color', False): - assert use_color('auto') is False + with envcontext.envcontext([('TERM', envcontext.UNSET)]): + assert use_color('auto') is False + + +def test_use_color_dumb_term(): + with mock.patch.object(sys.stdout, 'isatty', return_value=True): + with mock.patch('pre_commit.color.terminal_supports_color', True): + with envcontext.envcontext([('TERM', 'dumb')]): + assert use_color('auto') is False def test_use_color_raises_if_given_shenanigans(): From da80cc6479154c0a0a6096d183f9d1d72aae556b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 3 Aug 2019 11:41:54 -0700 Subject: [PATCH 451/544] Allow init-templatedir to be called outside of git --- pre_commit/main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 53c2dba56..dbfbecf6f 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -36,6 +36,9 @@ os.environ.pop('__PYVENV_LAUNCHER__', None) +COMMANDS_NO_GIT = {'clean', 'gc', 'init-templatedir', 'sample-config'} + + def _add_color_option(parser): parser.add_argument( '--color', default=os.environ.get('PRE_COMMIT_COLOR', 'auto'), @@ -273,7 +276,7 @@ def main(argv=None): parser.parse_args(['--help']) with error_handler(), logging_handler(args.color): - if args.command not in {'clean', 'gc', 'sample-config'}: + if args.command not in COMMANDS_NO_GIT: _adjust_args_and_chdir(args) git.check_for_cygwin_mismatch() From cab8036db39b7f20a803d1e545dcf23d0bdd216b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 3 Aug 2019 11:42:18 -0700 Subject: [PATCH 452/544] Don't treat unset init.templateDir as the current directory --- pre_commit/commands/init_templatedir.py | 10 ++++++++-- tests/commands/init_templatedir_test.py | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/init_templatedir.py b/pre_commit/commands/init_templatedir.py index c1b95621f..8fe20fdc2 100644 --- a/pre_commit/commands/init_templatedir.py +++ b/pre_commit/commands/init_templatedir.py @@ -2,6 +2,7 @@ import os.path from pre_commit.commands.install_uninstall import install +from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output logger = logging.getLogger('pre_commit') @@ -12,9 +13,14 @@ def init_templatedir(config_file, store, directory, hook_type): config_file, store, overwrite=True, hook_type=hook_type, skip_on_missing_config=True, git_dir=directory, ) - _, out, _ = cmd_output('git', 'config', 'init.templateDir', retcode=None) + try: + _, out, _ = cmd_output('git', 'config', 'init.templateDir') + except CalledProcessError: + configured_path = None + else: + configured_path = os.path.realpath(out.strip()) dest = os.path.realpath(directory) - if os.path.realpath(out.strip()) != dest: + if configured_path != dest: logger.warning('`init.templateDir` not set to the target directory') logger.warning( 'maybe `git config --global init.templateDir {}`?'.format(dest), diff --git a/tests/commands/init_templatedir_test.py b/tests/commands/init_templatedir_test.py index 2910ac9e2..9b5c7486b 100644 --- a/tests/commands/init_templatedir_test.py +++ b/tests/commands/init_templatedir_test.py @@ -47,3 +47,17 @@ def test_init_templatedir_already_set(tmpdir, tempdir_factory, store, cap_out): lines = cap_out.get().splitlines() assert len(lines) == 1 assert lines[0].startswith('pre-commit installed at') + + +def test_init_templatedir_not_set(tmpdir, store, cap_out): + # set HOME to ignore the current `.gitconfig` + with envcontext([('HOME', str(tmpdir))]): + with tmpdir.join('tmpl').ensure_dir().as_cwd(): + # we have not set init.templateDir so this should produce a warning + init_templatedir(C.CONFIG_FILE, store, '.', hook_type='pre-commit') + + lines = cap_out.get().splitlines() + assert len(lines) == 3 + assert lines[1] == ( + '[WARNING] `init.templateDir` not set to the target directory' + ) From f48c0abcbe21186478149083b79a5d82014b7ccf Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 3 Aug 2019 13:30:13 -0700 Subject: [PATCH 453/544] Use expanduser in init-templatedir like git does --- pre_commit/commands/init_templatedir.py | 2 +- tests/commands/init_templatedir_test.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/init_templatedir.py b/pre_commit/commands/init_templatedir.py index 8fe20fdc2..6e8df18cc 100644 --- a/pre_commit/commands/init_templatedir.py +++ b/pre_commit/commands/init_templatedir.py @@ -18,7 +18,7 @@ def init_templatedir(config_file, store, directory, hook_type): except CalledProcessError: configured_path = None else: - configured_path = os.path.realpath(out.strip()) + configured_path = os.path.realpath(os.path.expanduser(out.strip())) dest = os.path.realpath(directory) if configured_path != dest: logger.warning('`init.templateDir` not set to the target directory') diff --git a/tests/commands/init_templatedir_test.py b/tests/commands/init_templatedir_test.py index 9b5c7486b..b94de99af 100644 --- a/tests/commands/init_templatedir_test.py +++ b/tests/commands/init_templatedir_test.py @@ -1,5 +1,8 @@ +import os.path import subprocess +import mock + import pre_commit.constants as C from pre_commit.commands.init_templatedir import init_templatedir from pre_commit.envcontext import envcontext @@ -61,3 +64,18 @@ def test_init_templatedir_not_set(tmpdir, store, cap_out): assert lines[1] == ( '[WARNING] `init.templateDir` not set to the target directory' ) + + +def test_init_templatedir_expanduser(tmpdir, tempdir_factory, store, cap_out): + target = str(tmpdir.join('tmpl')) + tmp_git_dir = git_dir(tempdir_factory) + with cwd(tmp_git_dir): + cmd_output('git', 'config', 'init.templateDir', '~/templatedir') + with mock.patch.object(os.path, 'expanduser', return_value=target): + init_templatedir( + C.CONFIG_FILE, store, target, hook_type='pre-commit', + ) + + lines = cap_out.get().splitlines() + assert len(lines) == 1 + assert lines[0].startswith('pre-commit installed at') From 07f66417dd1cb29eaeb4414041a2d42e9e91f17d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 3 Aug 2019 15:21:26 -0700 Subject: [PATCH 454/544] v1.18.0 --- CHANGELOG.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc1a2d30f..057851eb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,47 @@ +1.18.0 - 2019-08-03 +=================== + +### Features +- Use the current running executable if it matches the requested + `language_version` + - #1062 PR by @asottile. +- Print the stage when a hook is not found + - #1078 issue by @madkinsz. + - #1079 PR by @madkinsz. +- `pre-commit autoupdate` now supports non-`master` default branches + - #1089 PR by @asottile. +- Add `pre-commit init-templatedir` which makes it easier to automatically + enable `pre-commit` in cloned repositories. + - #1084 issue by @ssbarnea. + - #1090 PR by @asottile. + - #1107 PR by @asottile. +- pre-commit's color can be controlled using + `PRE_COMMIT_COLOR={auto,always,never}` + - #1073 issue by @saper. + - #1092 PR by @geieredgar. + - #1098 PR by @geieredgar. +- pre-commit's color can now be disabled using `TERM=dumb` + - #1073 issue by @saper. + - #1103 PR by @asottile. +- pre-commit now supports `docker` based hooks on windows + - #1072 by @cz-fish. + - #1093 PR by @geieredgar. + +### Fixes +- Fix shallow clone + - #1077 PR by @asottile. +- Fix autoupdate version flip flop when using shallow cloning + - #1076 issue by @mxr. + - #1088 PR by @asottile. +- Fix autoupdate when the current revision is invalid + - #1088 PR by @asottile. + +### Misc. +- Replace development instructions with `tox --devenv ...` + - #1032 issue by @yoavcaspi. + - #1067 PR by @asottile. + + 1.17.0 - 2019-06-06 =================== diff --git a/setup.cfg b/setup.cfg index 3793677e9..f7d451712 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.17.0 +version = 1.18.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From cbbfcd20b4e6393674214c78a43202605d475156 Mon Sep 17 00:00:00 2001 From: zimbatm Date: Thu, 8 Aug 2019 17:24:39 +0200 Subject: [PATCH 455/544] rust language: use the new cargo install command cargo install now requires an additional `--path ` argument. --- pre_commit/languages/rust.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index e09d0078f..4b25a9d10 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -73,7 +73,7 @@ def install_environment(prefix, version, additional_dependencies): _add_dependencies(prefix.path('Cargo.toml'), lib_deps) with clean_path_on_failure(directory): - packages_to_install = {()} + packages_to_install = {('--path', '.')} for cli_dep in cli_deps: cli_dep = cli_dep[len('cli:'):] package, _, version = cli_dep.partition(':') From 7c69730ad27cafafe589a9726878cb235c93d916 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 11 Aug 2019 14:07:20 -0700 Subject: [PATCH 456/544] v1.18.1 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 057851eb7..ba491cfba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +1.18.1 - 2019-08-11 +=================== + +### Fixes +- Fix installation of `rust` hooks with new `cargo` + - #1112 issue by @zimbatm. + - #1113 PR by @zimbatm. + 1.18.0 - 2019-08-03 =================== diff --git a/setup.cfg b/setup.cfg index f7d451712..c7175b24b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.18.0 +version = 1.18.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From dd46fde3846fd7742033014bd10b6fc827b7229a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 15 Aug 2019 08:26:01 +0300 Subject: [PATCH 457/544] Spelling fixes --- CHANGELOG.md | 4 ++-- tests/staged_files_only_test.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba491cfba..697e3cd9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -261,7 +261,7 @@ ### Features - Run hooks in parallel - - individual hooks may opt out of parallel exection with `require_serial: true` + - individual hooks may opt out of parallel execution with `require_serial: true` - #510 issue by @chriskuehl. - #851 PR by @chriskuehl. @@ -440,7 +440,7 @@ ### Fixes - Fix integration with go 1.10 and `pkg` directory - #725 PR by @asottile -- Restore support for `git<1.8.5` (inadvertantly removed in 1.7.0) +- Restore support for `git<1.8.5` (inadvertently removed in 1.7.0) - #723 issue by @JohnLyman. - #724 PR by @asottile. diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 2410bffec..107c14914 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -328,7 +328,7 @@ def test_whitespace_errors(in_git_dir, patch_dir): test_crlf(in_git_dir, patch_dir, True, True, 'true') -def test_autocrlf_commited_crlf(in_git_dir, patch_dir): +def test_autocrlf_committed_crlf(in_git_dir, patch_dir): """Regression test for #570""" cmd_output('git', 'config', '--local', 'core.autocrlf', 'false') _write(b'1\r\n2\r\n') From fa2e154b419238532cba9664fd444bcc00dfb787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 15 Aug 2019 08:36:06 +0300 Subject: [PATCH 458/544] Stabilize python default version lookup For example, for sys.executable: /usr/bin/python3 -> python3.7 ...the default lookup may return either python3 or python3.7. Make the order deterministic by iterating over tuple, not set, of candidates. --- pre_commit/languages/python.py | 12 +++++++++--- tests/languages/python_test.py | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 5d48fb892..df00a0710 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -43,14 +43,13 @@ def _find_by_py_launcher(version): # pragma: no cover (windows only) pass -def _get_default_version(): # pragma: no cover (platform dependent) +def _find_by_sys_executable(): def _norm(path): _, exe = os.path.split(path.lower()) exe, _, _ = exe.partition('.exe') if find_executable(exe) and exe not in {'python', 'pythonw'}: return exe - # First attempt from `sys.executable` (or the realpath) # On linux, I see these common sys.executables: # # system `python`: /usr/bin/python -> python2.7 @@ -59,10 +58,17 @@ def _norm(path): # virtualenv v -ppython2: v/bin/python -> python2 # virtualenv v -ppython2.7: v/bin/python -> python2.7 # virtualenv v -ppypy: v/bin/python -> v/bin/pypy - for path in {sys.executable, os.path.realpath(sys.executable)}: + for path in (sys.executable, os.path.realpath(sys.executable)): exe = _norm(path) if exe: return exe + return None + + +def _get_default_version(): # pragma: no cover (platform dependent) + + # First attempt from `sys.executable` (or the realpath) + exe = _find_by_sys_executable() # Next try the `pythonX.X` executable exe = 'python{}.{}'.format(*sys.version_info) diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 426d3ec6f..52e0e85c6 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -32,3 +32,18 @@ def test_sys_executable_matches(v): def test_sys_executable_matches_does_not_match(v): with mock.patch.object(sys, 'version_info', (3, 6, 7)): assert not python._sys_executable_matches(v) + + +@pytest.mark.parametrize( + 'exe,realpath,expected', ( + ('/usr/bin/python3', '/usr/bin/python3.7', 'python3'), + ('/usr/bin/python', '/usr/bin/python3.7', 'python3.7'), + ('/usr/bin/python', '/usr/bin/python', None), + ('/usr/bin/python3.6m', '/usr/bin/python3.6m', 'python3.6m'), + ('v/bin/python', 'v/bin/pypy', 'pypy'), + ), +) +def test_find_by_sys_executable(exe, realpath, expected): + with mock.patch.object(sys, 'executable', exe): + with mock.patch('os.path.realpath', return_value=realpath): + assert python._find_by_sys_executable() == expected From c3778308980be22f0d2708b7ffeed2d3e11e60fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 15 Aug 2019 18:30:43 +0300 Subject: [PATCH 459/544] Mock find_executable for find_by_sys_executable test --- tests/languages/python_test.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 52e0e85c6..4506f9f08 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -44,6 +44,12 @@ def test_sys_executable_matches_does_not_match(v): ), ) def test_find_by_sys_executable(exe, realpath, expected): + def mocked_find_executable(exe): + return exe.rpartition('/')[2] with mock.patch.object(sys, 'executable', exe): with mock.patch('os.path.realpath', return_value=realpath): - assert python._find_by_sys_executable() == expected + with mock.patch( + 'pre_commit.parse_shebang.find_executable', + side_effect=mocked_find_executable, + ): + assert python._find_by_sys_executable() == expected From 38da98d2d65d9df37671aba3f10fbbd080fadd4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 15 Aug 2019 18:43:31 +0300 Subject: [PATCH 460/544] Address @asottile's review comments --- pre_commit/languages/python.py | 1 - tests/languages/python_test.py | 9 +++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index df00a0710..1585a7fc7 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -66,7 +66,6 @@ def _norm(path): def _get_default_version(): # pragma: no cover (platform dependent) - # First attempt from `sys.executable` (or the realpath) exe = _find_by_sys_executable() diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 4506f9f08..3634fa4f0 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -7,6 +7,7 @@ import mock import pytest +import pre_commit.parse_shebang from pre_commit.languages import python @@ -35,7 +36,7 @@ def test_sys_executable_matches_does_not_match(v): @pytest.mark.parametrize( - 'exe,realpath,expected', ( + ('exe', 'realpath', 'expected'), ( ('/usr/bin/python3', '/usr/bin/python3.7', 'python3'), ('/usr/bin/python', '/usr/bin/python3.7', 'python3.7'), ('/usr/bin/python', '/usr/bin/python', None), @@ -47,9 +48,9 @@ def test_find_by_sys_executable(exe, realpath, expected): def mocked_find_executable(exe): return exe.rpartition('/')[2] with mock.patch.object(sys, 'executable', exe): - with mock.patch('os.path.realpath', return_value=realpath): - with mock.patch( - 'pre_commit.parse_shebang.find_executable', + with mock.patch.object(os.path, 'realpath', return_value=realpath): + with mock.patch.object( + pre_commit.parse_shebang, 'find_executable', side_effect=mocked_find_executable, ): assert python._find_by_sys_executable() == expected From 562276098c5c42f364cdf836e1842d30265fd4ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 15 Aug 2019 18:54:08 +0300 Subject: [PATCH 461/544] Address more @asottile's review comments --- pre_commit/languages/python.py | 2 ++ tests/languages/python_test.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 1585a7fc7..6d125a439 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -68,6 +68,8 @@ def _norm(path): def _get_default_version(): # pragma: no cover (platform dependent) # First attempt from `sys.executable` (or the realpath) exe = _find_by_sys_executable() + if exe: + return exe # Next try the `pythonX.X` executable exe = 'python{}.{}'.format(*sys.version_info) diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 3634fa4f0..debf9753d 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -7,7 +7,7 @@ import mock import pytest -import pre_commit.parse_shebang +from pre_commit import parse_shebang from pre_commit.languages import python @@ -50,7 +50,7 @@ def mocked_find_executable(exe): with mock.patch.object(sys, 'executable', exe): with mock.patch.object(os.path, 'realpath', return_value=realpath): with mock.patch.object( - pre_commit.parse_shebang, 'find_executable', + parse_shebang, 'find_executable', side_effect=mocked_find_executable, ): assert python._find_by_sys_executable() == expected From f84b19748d7d0dfda496b73ab365a2e64b377696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 15 Aug 2019 19:28:07 +0300 Subject: [PATCH 462/544] Patch the correct find_executable --- tests/languages/python_test.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index debf9753d..d9d8ecd5b 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -7,7 +7,6 @@ import mock import pytest -from pre_commit import parse_shebang from pre_commit.languages import python @@ -45,12 +44,7 @@ def test_sys_executable_matches_does_not_match(v): ), ) def test_find_by_sys_executable(exe, realpath, expected): - def mocked_find_executable(exe): - return exe.rpartition('/')[2] with mock.patch.object(sys, 'executable', exe): with mock.patch.object(os.path, 'realpath', return_value=realpath): - with mock.patch.object( - parse_shebang, 'find_executable', - side_effect=mocked_find_executable, - ): + with mock.patch.object(python, 'find_executable', lambda x: x): assert python._find_by_sys_executable() == expected From 7f900395ec8fa2de7962694e11a206af33dc9fcd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 15 Aug 2019 10:07:24 -0700 Subject: [PATCH 463/544] v1.18.2 --- CHANGELOG.md | 7 +++++++ setup.cfg | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 697e3cd9c..dd3d02c90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +1.18.2 - 2019-08-15 +=================== + +### Fixes +- Make default python lookup more deterministic to avoid redundant installs + - #1117 PR by @scop. + 1.18.1 - 2019-08-11 =================== diff --git a/setup.cfg b/setup.cfg index c7175b24b..348787b62 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.18.1 +version = 1.18.2 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From b0c7ae4d2912cf7e3693595c7a5bf191feff5db0 Mon Sep 17 00:00:00 2001 From: Henry Tang Date: Wed, 28 Aug 2019 00:03:04 +0800 Subject: [PATCH 464/544] Fix NODE_PATH on win32 --- pre_commit/languages/node.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index cd3b7b541..00f32340c 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -24,18 +24,20 @@ def _envdir(prefix, version): def get_env_patch(venv): # pragma: windows no cover + lib_dir = 'lib' if sys.platform == 'cygwin': # pragma: no cover _, win_venv, _ = cmd_output('cygpath', '-w', venv) install_prefix = r'{}\bin'.format(win_venv.strip()) elif sys.platform == 'win32': # pragma: no cover install_prefix = bin_dir(venv) + lib_dir = 'Scripts' else: # pragma: windows no cover install_prefix = venv return ( ('NODE_VIRTUAL_ENV', venv), ('NPM_CONFIG_PREFIX', install_prefix), ('npm_config_prefix', install_prefix), - ('NODE_PATH', os.path.join(venv, 'lib', 'node_modules')), + ('NODE_PATH', os.path.join(venv, lib_dir, 'node_modules')), ('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))), ) From 8537e7c94edc7687c7e0d0c9bffec8a0545854d7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 27 Aug 2019 10:35:40 -0700 Subject: [PATCH 465/544] Simplify if statement slightly --- pre_commit/languages/node.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 00f32340c..7d85a327b 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -24,15 +24,16 @@ def _envdir(prefix, version): def get_env_patch(venv): # pragma: windows no cover - lib_dir = 'lib' if sys.platform == 'cygwin': # pragma: no cover _, win_venv, _ = cmd_output('cygpath', '-w', venv) install_prefix = r'{}\bin'.format(win_venv.strip()) + lib_dir = 'lib' elif sys.platform == 'win32': # pragma: no cover install_prefix = bin_dir(venv) lib_dir = 'Scripts' else: # pragma: windows no cover install_prefix = venv + lib_dir = 'lib' return ( ('NODE_VIRTUAL_ENV', venv), ('NPM_CONFIG_PREFIX', install_prefix), From 0245a6783130975c786b9140e2d8695473b2c105 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 27 Aug 2019 10:38:53 -0700 Subject: [PATCH 466/544] v1.18.3 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd3d02c90..5f7811cbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +1.18.3 - 2019-08-27 +=================== + +### Fixes +- Fix `node_modules` plugin installation on windows + - #1123 issue by @henryykt. + - #1122 PR by @henryykt. + 1.18.2 - 2019-08-15 =================== diff --git a/setup.cfg b/setup.cfg index 348787b62..0c7738421 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.18.2 +version = 1.18.3 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From c1580be7d396e9b8b5f4d662f5c5b2f842239dc3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 27 Aug 2019 21:28:06 -0700 Subject: [PATCH 467/544] Remove redundant flake8 dependency Committed via https://github.com/asottile/all-repos --- requirements-dev.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 157f287d3..ba80df7f3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,6 @@ -e . coverage -flake8 mock pytest pytest-env From d3474dfff339acb056c93f396bc889abcafac069 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 28 Aug 2019 11:41:03 -0700 Subject: [PATCH 468/544] make the tests not depend on flake8 being installed --- tests/commands/run_test.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 94d44e150..49ce008c4 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -3,6 +3,7 @@ import io import os.path +import pipes import subprocess import sys @@ -642,9 +643,11 @@ def test_local_hook_passes(cap_out, store, repo_with_passing_hook): 'repo': 'local', 'hooks': [ { - 'id': 'flake8', - 'name': 'flake8', - 'entry': "'{}' -m flake8".format(sys.executable), + 'id': 'identity-copy', + 'name': 'identity-copy', + 'entry': '{} -m pre_commit.meta_hooks.identity'.format( + pipes.quote(sys.executable), + ), 'language': 'system', 'files': r'\.py$', }, @@ -869,10 +872,13 @@ def test_args_hook_only(cap_out, store, repo_with_passing_hook): 'repo': 'local', 'hooks': [ { - 'id': 'flake8', - 'name': 'flake8', - 'entry': "'{}' -m flake8".format(sys.executable), + 'id': 'identity-copy', + 'name': 'identity-copy', + 'entry': '{} -m pre_commit.meta_hooks.identity'.format( + pipes.quote(sys.executable), + ), 'language': 'system', + 'files': r'\.py$', 'stages': ['commit'], }, { @@ -891,4 +897,4 @@ def test_args_hook_only(cap_out, store, repo_with_passing_hook): repo_with_passing_hook, run_opts(hook='do_not_commit'), ) - assert b'flake8' not in printed + assert b'identity-copy' not in printed From 247d45af0595c88b8880324a0757a17004a3f403 Mon Sep 17 00:00:00 2001 From: marqueewinq Date: Fri, 20 Sep 2019 15:05:51 +0300 Subject: [PATCH 469/544] fixed #1141 --- pre_commit/error_handler.py | 5 +++++ tests/error_handler_test.py | 34 +++++++++++++++++++++++----------- tests/main_test.py | 9 ++++++--- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 946f134cb..3f5cfe209 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -4,12 +4,14 @@ import contextlib import os.path +import sys import traceback import six from pre_commit import five from pre_commit import output +from pre_commit.constants import VERSION as pre_commit_version from pre_commit.store import Store @@ -29,6 +31,9 @@ def _log_and_exit(msg, exc, formatted): five.to_bytes(msg), b': ', five.to_bytes(type(exc).__name__), b': ', _to_bytes(exc), b'\n', + _to_bytes('pre-commit.version={}\n'.format(pre_commit_version)), + _to_bytes('sys.version={}\n'.format(sys.version.replace('\n', ' '))), + _to_bytes('sys.executable={}\n'.format(sys.executable)), )) output.write(error_msg) store = Store() diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 1b222f904..244859cfe 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -104,17 +104,29 @@ def test_log_and_exit(cap_out, mock_store_dir): printed = cap_out.get() log_file = os.path.join(mock_store_dir, 'pre-commit.log') - assert printed == ( - 'msg: FatalError: hai\n' - 'Check the log at {}\n'.format(log_file) - ) + printed_lines = printed.split('\n') + assert len(printed_lines) == 6, printed_lines + assert printed_lines[0] == 'msg: FatalError: hai' + assert re.match(r'^pre-commit.version=\d+\.\d+\.\d+$', printed_lines[1]) + assert printed_lines[2].startswith('sys.version=') + assert printed_lines[3].startswith('sys.executable=') + assert printed_lines[4] == 'Check the log at {}'.format(log_file) + assert printed_lines[5] == '' # checks for \n at the end of last line assert os.path.exists(log_file) with io.open(log_file) as f: - assert f.read() == ( - 'msg: FatalError: hai\n' - "I'm a stacktrace\n" + logged_lines = f.read().split('\n') + assert len(logged_lines) == 6, logged_lines + assert logged_lines[0] == 'msg: FatalError: hai' + assert re.match( + r'^pre-commit.version=\d+\.\d+\.\d+$', + printed_lines[1], ) + assert logged_lines[2].startswith('sys.version=') + assert logged_lines[3].startswith('sys.executable=') + assert logged_lines[4] == "I'm a stacktrace" + # checks for \n at the end of stack trace + assert printed_lines[5] == '' def test_error_handler_non_ascii_exception(mock_store_dir): @@ -136,7 +148,7 @@ def test_error_handler_no_tty(tempdir_factory): pre_commit_home=pre_commit_home, ) log_file = os.path.join(pre_commit_home, 'pre-commit.log') - assert output[1].replace('\r', '') == ( - 'An unexpected error has occurred: ValueError: ☃\n' - 'Check the log at {}\n'.format(log_file) - ) + output_lines = output[1].replace('\r', '').split('\n') + assert output_lines[0] == 'An unexpected error has occurred: ValueError: ☃' + assert output_lines[-2] == 'Check the log at {}'.format(log_file) + assert output_lines[-1] == '' # checks for \n at the end of stack trace diff --git a/tests/main_test.py b/tests/main_test.py index 75fd56001..7ebd0ef44 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -164,11 +164,14 @@ def test_expected_fatal_error_no_git_repo(in_tmpdir, cap_out, mock_store_dir): with pytest.raises(SystemExit): main.main([]) log_file = os.path.join(mock_store_dir, 'pre-commit.log') - assert cap_out.get() == ( + cap_out_lines = cap_out.get().split('\n') + assert ( + cap_out_lines[0] == 'An error has occurred: FatalError: git failed. ' - 'Is it installed, and are you in a Git repository directory?\n' - 'Check the log at {}\n'.format(log_file) + 'Is it installed, and are you in a Git repository directory?' ) + assert cap_out_lines[-2] == 'Check the log at {}'.format(log_file) + assert cap_out_lines[-1] == '' # checks for \n at the end of error message def test_warning_on_tags_only(mock_commands, cap_out, mock_store_dir): From a18646deb2603c9c13d9aa4af8b0c23bdceab603 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 23 Sep 2019 11:14:36 -0700 Subject: [PATCH 470/544] Allow --hook-type to be specified multiple times --- pre_commit/commands/init_templatedir.py | 6 +- pre_commit/commands/install_uninstall.py | 46 +++++---- pre_commit/main.py | 16 +++- tests/commands/init_templatedir_test.py | 12 ++- tests/commands/install_uninstall_test.py | 114 +++++++++++++---------- tests/commands/run_test.py | 4 +- 6 files changed, 119 insertions(+), 79 deletions(-) diff --git a/pre_commit/commands/init_templatedir.py b/pre_commit/commands/init_templatedir.py index 6e8df18cc..74a32f2b6 100644 --- a/pre_commit/commands/init_templatedir.py +++ b/pre_commit/commands/init_templatedir.py @@ -8,10 +8,10 @@ logger = logging.getLogger('pre_commit') -def init_templatedir(config_file, store, directory, hook_type): +def init_templatedir(config_file, store, directory, hook_types): install( - config_file, store, overwrite=True, hook_type=hook_type, - skip_on_missing_config=True, git_dir=directory, + config_file, store, hook_types=hook_types, + overwrite=True, skip_on_missing_config=True, git_dir=directory, ) try: _, out, _ = cmd_output('git', 'config', 'init.templateDir') diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 9b2c3b807..0fda6272a 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -67,19 +67,10 @@ def shebang(): return '#!/usr/bin/env {}'.format(py) -def install( - config_file, store, - overwrite=False, hooks=False, hook_type='pre-commit', - skip_on_missing_config=False, git_dir=None, +def _install_hook_script( + config_file, hook_type, + overwrite=False, skip_on_missing_config=False, git_dir=None, ): - """Install the pre-commit hooks.""" - if cmd_output('git', 'config', 'core.hooksPath', retcode=None)[1].strip(): - logger.error( - 'Cowardly refusing to install hooks with `core.hooksPath` set.\n' - 'hint: `git config --unset-all core.hooksPath`', - ) - return 1 - hook_path, legacy_path = _hook_paths(hook_type, git_dir=git_dir) mkdirp(os.path.dirname(hook_path)) @@ -120,7 +111,27 @@ def install( output.write_line('pre-commit installed at {}'.format(hook_path)) - # If they requested we install all of the hooks, do so. + +def install( + config_file, store, hook_types, + overwrite=False, hooks=False, + skip_on_missing_config=False, git_dir=None, +): + if cmd_output('git', 'config', 'core.hooksPath', retcode=None)[1].strip(): + logger.error( + 'Cowardly refusing to install hooks with `core.hooksPath` set.\n' + 'hint: `git config --unset-all core.hooksPath`', + ) + return 1 + + for hook_type in hook_types: + _install_hook_script( + config_file, hook_type, + overwrite=overwrite, + skip_on_missing_config=skip_on_missing_config, + git_dir=git_dir, + ) + if hooks: install_hooks(config_file, store) @@ -131,13 +142,12 @@ def install_hooks(config_file, store): install_hook_envs(all_hooks(load_config(config_file), store), store) -def uninstall(hook_type='pre-commit'): - """Uninstall the pre-commit hooks.""" +def _uninstall_hook_script(hook_type): # type: (str) -> None hook_path, legacy_path = _hook_paths(hook_type) # If our file doesn't exist or it isn't ours, gtfo. if not os.path.exists(hook_path) or not is_our_script(hook_path): - return 0 + return os.remove(hook_path) output.write_line('{} uninstalled'.format(hook_type)) @@ -146,4 +156,8 @@ def uninstall(hook_type='pre-commit'): os.rename(legacy_path, hook_path) output.write_line('Restored previous hooks to {}'.format(hook_path)) + +def uninstall(hook_types): + for hook_type in hook_types: + _uninstall_hook_script(hook_type) return 0 diff --git a/pre_commit/main.py b/pre_commit/main.py index dbfbecf6f..8d2d63020 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -60,7 +60,8 @@ def _add_hook_type_option(parser): '-t', '--hook-type', choices=( 'pre-commit', 'pre-push', 'prepare-commit-msg', 'commit-msg', ), - default='pre-commit', + action='append', + dest='hook_types', ) @@ -120,6 +121,11 @@ def _adjust_args_and_chdir(args): args.files = [os.path.relpath(filename) for filename in args.files] if args.command == 'try-repo' and os.path.exists(args.repo): args.repo = os.path.relpath(args.repo) + if ( + args.command in {'install', 'uninstall', 'init-templatedir'} and + not args.hook_types + ): + args.hook_types = ['pre-commit'] def main(argv=None): @@ -299,14 +305,14 @@ def main(argv=None): elif args.command == 'install': return install( args.config, store, + hook_types=args.hook_types, overwrite=args.overwrite, hooks=args.install_hooks, - hook_type=args.hook_type, skip_on_missing_config=args.allow_missing_config, ) elif args.command == 'init-templatedir': return init_templatedir( - args.config, store, - args.directory, hook_type=args.hook_type, + args.config, store, args.directory, + hook_types=args.hook_types, ) elif args.command == 'install-hooks': return install_hooks(args.config, store) @@ -319,7 +325,7 @@ def main(argv=None): elif args.command == 'try-repo': return try_repo(args) elif args.command == 'uninstall': - return uninstall(hook_type=args.hook_type) + return uninstall(hook_types=args.hook_types) else: raise NotImplementedError( 'Command {} not implemented.'.format(args.command), diff --git a/tests/commands/init_templatedir_test.py b/tests/commands/init_templatedir_test.py index b94de99af..1bb9695fe 100644 --- a/tests/commands/init_templatedir_test.py +++ b/tests/commands/init_templatedir_test.py @@ -16,7 +16,7 @@ def test_init_templatedir(tmpdir, tempdir_factory, store, cap_out): target = str(tmpdir.join('tmpl')) - init_templatedir(C.CONFIG_FILE, store, target, hook_type='pre-commit') + init_templatedir(C.CONFIG_FILE, store, target, hook_types=['pre-commit']) lines = cap_out.get().splitlines() assert lines[0].startswith('pre-commit installed at ') assert lines[1] == ( @@ -45,7 +45,9 @@ def test_init_templatedir_already_set(tmpdir, tempdir_factory, store, cap_out): tmp_git_dir = git_dir(tempdir_factory) with cwd(tmp_git_dir): cmd_output('git', 'config', 'init.templateDir', target) - init_templatedir(C.CONFIG_FILE, store, target, hook_type='pre-commit') + init_templatedir( + C.CONFIG_FILE, store, target, hook_types=['pre-commit'], + ) lines = cap_out.get().splitlines() assert len(lines) == 1 @@ -57,7 +59,9 @@ def test_init_templatedir_not_set(tmpdir, store, cap_out): with envcontext([('HOME', str(tmpdir))]): with tmpdir.join('tmpl').ensure_dir().as_cwd(): # we have not set init.templateDir so this should produce a warning - init_templatedir(C.CONFIG_FILE, store, '.', hook_type='pre-commit') + init_templatedir( + C.CONFIG_FILE, store, '.', hook_types=['pre-commit'], + ) lines = cap_out.get().splitlines() assert len(lines) == 3 @@ -73,7 +77,7 @@ def test_init_templatedir_expanduser(tmpdir, tempdir_factory, store, cap_out): cmd_output('git', 'config', 'init.templateDir', '~/templatedir') with mock.patch.object(os.path, 'expanduser', return_value=target): init_templatedir( - C.CONFIG_FILE, store, target, hook_type='pre-commit', + C.CONFIG_FILE, store, target, hook_types=['pre-commit'], ) lines = cap_out.get().splitlines() diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 913bf74eb..52f6e4e57 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -67,10 +67,10 @@ def test_shebang_posix_on_path(tmpdir): def test_install_pre_commit(in_git_dir, store): - assert not install(C.CONFIG_FILE, store) + assert not install(C.CONFIG_FILE, store, hook_types=['pre-commit']) assert os.access(in_git_dir.join('.git/hooks/pre-commit').strpath, os.X_OK) - assert not install(C.CONFIG_FILE, store, hook_type='pre-push') + assert not install(C.CONFIG_FILE, store, hook_types=['pre-push']) assert os.access(in_git_dir.join('.git/hooks/pre-push').strpath, os.X_OK) @@ -78,32 +78,41 @@ def test_install_hooks_directory_not_present(in_git_dir, store): # Simulate some git clients which don't make .git/hooks #234 if in_git_dir.join('.git/hooks').exists(): # pragma: no cover (odd git) in_git_dir.join('.git/hooks').remove() - install(C.CONFIG_FILE, store) + install(C.CONFIG_FILE, store, hook_types=['pre-commit']) assert in_git_dir.join('.git/hooks/pre-commit').exists() +def test_install_multiple_hooks_at_once(in_git_dir, store): + install(C.CONFIG_FILE, store, hook_types=['pre-commit', 'pre-push']) + assert in_git_dir.join('.git/hooks/pre-commit').exists() + assert in_git_dir.join('.git/hooks/pre-push').exists() + uninstall(hook_types=['pre-commit', 'pre-push']) + assert not in_git_dir.join('.git/hooks/pre-commit').exists() + assert not in_git_dir.join('.git/hooks/pre-push').exists() + + def test_install_refuses_core_hookspath(in_git_dir, store): cmd_output('git', 'config', '--local', 'core.hooksPath', 'hooks') - assert install(C.CONFIG_FILE, store) + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) @xfailif_no_symlink # pragma: windows no cover def test_install_hooks_dead_symlink(in_git_dir, store): hook = in_git_dir.join('.git/hooks').ensure_dir().join('pre-commit') os.symlink('/fake/baz', hook.strpath) - install(C.CONFIG_FILE, store) + install(C.CONFIG_FILE, store, hook_types=['pre-commit']) assert hook.exists() def test_uninstall_does_not_blow_up_when_not_there(in_git_dir): - assert uninstall() == 0 + assert uninstall(hook_types=['pre-commit']) == 0 def test_uninstall(in_git_dir, store): assert not in_git_dir.join('.git/hooks/pre-commit').exists() - install(C.CONFIG_FILE, store) + install(C.CONFIG_FILE, store, hook_types=['pre-commit']) assert in_git_dir.join('.git/hooks/pre-commit').exists() - uninstall() + uninstall(hook_types=['pre-commit']) assert not in_git_dir.join('.git/hooks/pre-commit').exists() @@ -142,7 +151,7 @@ def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): def test_install_pre_commit_and_run(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -152,9 +161,9 @@ def test_install_pre_commit_and_run(tempdir_factory, store): def test_install_pre_commit_and_run_custom_path(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - cmd_output('git', 'mv', C.CONFIG_FILE, 'custom-config.yaml') + cmd_output('git', 'mv', C.CONFIG_FILE, 'custom.yaml') git_commit(cwd=path) - assert install('custom-config.yaml', store) == 0 + assert install('custom.yaml', store, hook_types=['pre-commit']) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -169,7 +178,7 @@ def test_install_in_submodule_and_run(tempdir_factory, store): sub_pth = os.path.join(parent_path, 'sub') with cwd(sub_pth): - assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) @@ -182,7 +191,7 @@ def test_install_in_worktree_and_run(tempdir_factory, store): cmd_output('git', '-C', src_path, 'worktree', 'add', path, '-b', 'master') with cwd(path): - assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) @@ -199,7 +208,7 @@ def test_commit_am(tempdir_factory, store): with io.open('unstaged', 'w') as foo_file: foo_file.write('Oh hai') - assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -208,7 +217,7 @@ def test_commit_am(tempdir_factory, store): def test_unicode_merge_commit_message(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 cmd_output('git', 'checkout', 'master', '-b', 'foo') git_commit('-n', cwd=path) cmd_output('git', 'checkout', 'master') @@ -225,8 +234,8 @@ def test_unicode_merge_commit_message(tempdir_factory, store): def test_install_idempotent(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - assert install(C.CONFIG_FILE, store) == 0 - assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -252,7 +261,7 @@ def test_environment_not_sourced(tempdir_factory, store): with cwd(path): # Patch the executable to simulate rming virtualenv with mock.patch.object(sys, 'executable', '/does-not-exist'): - assert install(C.CONFIG_FILE, store) == 0 + assert not install(C.CONFIG_FILE, store, hook_types=['pre-commit']) # Use a specific homedir to ignore --user installs homedir = tempdir_factory.get() @@ -290,7 +299,7 @@ def test_environment_not_sourced(tempdir_factory, store): def test_failing_hooks_returns_nonzero(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'failing_hook_repo') with cwd(path): - assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 1 @@ -323,7 +332,7 @@ def test_install_existing_hooks_no_overwrite(tempdir_factory, store): assert EXISTING_COMMIT_RUN.match(output) # Now install pre-commit (no-overwrite) - assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 # We should run both the legacy and pre-commit hooks ret, output = _get_commit_output(tempdir_factory) @@ -336,10 +345,10 @@ def test_legacy_overwriting_legacy_hook(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): _write_legacy_hook(path) - assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 _write_legacy_hook(path) # this previously crashed on windows. See #1010 - assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory, store): @@ -348,8 +357,8 @@ def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory, store): _write_legacy_hook(path) # Install twice - assert install(C.CONFIG_FILE, store) == 0 - assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 # We should run both the legacy and pre-commit hooks ret, output = _get_commit_output(tempdir_factory) @@ -374,7 +383,7 @@ def test_failing_existing_hook_returns_1(tempdir_factory, store): f.write('#!/usr/bin/env bash\necho "fail!"\nexit 1\n') make_executable(f.name) - assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 # We should get a failure from the legacy hook ret, output = _get_commit_output(tempdir_factory) @@ -385,7 +394,9 @@ def test_failing_existing_hook_returns_1(tempdir_factory, store): def test_install_overwrite_no_existing_hooks(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - assert install(C.CONFIG_FILE, store, overwrite=True) == 0 + assert not install( + C.CONFIG_FILE, store, hook_types=['pre-commit'], overwrite=True, + ) ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -396,7 +407,9 @@ def test_install_overwrite(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): _write_legacy_hook(path) - assert install(C.CONFIG_FILE, store, overwrite=True) == 0 + assert not install( + C.CONFIG_FILE, store, hook_types=['pre-commit'], overwrite=True, + ) ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -409,8 +422,8 @@ def test_uninstall_restores_legacy_hooks(tempdir_factory, store): _write_legacy_hook(path) # Now install and uninstall pre-commit - assert install(C.CONFIG_FILE, store) == 0 - assert uninstall() == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 + assert uninstall(hook_types=['pre-commit']) == 0 # Make sure we installed the "old" hook correctly ret, output = _get_commit_output(tempdir_factory, touch_file='baz') @@ -433,7 +446,7 @@ def test_replace_old_commit_script(tempdir_factory, store): make_executable(f.name) # Install normally - assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -445,7 +458,7 @@ def test_uninstall_doesnt_remove_not_our_hooks(in_git_dir): pre_commit.write('#!/usr/bin/env bash\necho 1\n') make_executable(pre_commit.strpath) - assert uninstall() == 0 + assert uninstall(hook_types=['pre-commit']) == 0 assert pre_commit.exists() @@ -461,7 +474,7 @@ def test_uninstall_doesnt_remove_not_our_hooks(in_git_dir): def test_installs_hooks_with_hooks_True(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - install(C.CONFIG_FILE, store, hooks=True) + install(C.CONFIG_FILE, store, hook_types=['pre-commit'], hooks=True) ret, output = _get_commit_output( tempdir_factory, pre_commit_home=store.directory, ) @@ -473,7 +486,7 @@ def test_installs_hooks_with_hooks_True(tempdir_factory, store): def test_install_hooks_command(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - install(C.CONFIG_FILE, store) + install(C.CONFIG_FILE, store, hook_types=['pre-commit']) install_hooks(C.CONFIG_FILE, store) ret, output = _get_commit_output( tempdir_factory, pre_commit_home=store.directory, @@ -486,7 +499,7 @@ def test_install_hooks_command(tempdir_factory, store): def test_installed_from_venv(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - install(C.CONFIG_FILE, store) + install(C.CONFIG_FILE, store, hook_types=['pre-commit']) # No environment so pre-commit is not on the path when running! # Should still pick up the python from when we installed ret, output = _get_commit_output( @@ -525,7 +538,7 @@ def test_pre_push_integration_failing(tempdir_factory, store): path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - install(C.CONFIG_FILE, store, hook_type='pre-push') + install(C.CONFIG_FILE, store, hook_types=['pre-push']) # commit succeeds because pre-commit is only installed for pre-push assert _get_commit_output(tempdir_factory)[0] == 0 assert _get_commit_output(tempdir_factory, touch_file='zzz')[0] == 0 @@ -543,7 +556,7 @@ def test_pre_push_integration_accepted(tempdir_factory, store): path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - install(C.CONFIG_FILE, store, hook_type='pre-push') + install(C.CONFIG_FILE, store, hook_types=['pre-push']) assert _get_commit_output(tempdir_factory)[0] == 0 retc, output = _get_push_output(tempdir_factory) @@ -563,7 +576,7 @@ def test_pre_push_force_push_without_fetch(tempdir_factory, store): assert _get_push_output(tempdir_factory)[0] == 0 with cwd(path2): - install(C.CONFIG_FILE, store, hook_type='pre-push') + install(C.CONFIG_FILE, store, hook_types=['pre-push']) assert _get_commit_output(tempdir_factory, msg='force!')[0] == 0 retc, output = _get_push_output(tempdir_factory, opts=('--force',)) @@ -578,7 +591,7 @@ def test_pre_push_new_upstream(tempdir_factory, store): path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - install(C.CONFIG_FILE, store, hook_type='pre-push') + install(C.CONFIG_FILE, store, hook_types=['pre-push']) assert _get_commit_output(tempdir_factory)[0] == 0 cmd_output('git', 'remote', 'rename', 'origin', 'upstream') @@ -594,7 +607,7 @@ def test_pre_push_integration_empty_push(tempdir_factory, store): path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - install(C.CONFIG_FILE, store, hook_type='pre-push') + install(C.CONFIG_FILE, store, hook_types=['pre-push']) _get_push_output(tempdir_factory) retc, output = _get_push_output(tempdir_factory) assert output == 'Everything up-to-date\n' @@ -617,7 +630,7 @@ def test_pre_push_legacy(tempdir_factory, store): ) make_executable(f.name) - install(C.CONFIG_FILE, store, hook_type='pre-push') + install(C.CONFIG_FILE, store, hook_types=['pre-push']) assert _get_commit_output(tempdir_factory)[0] == 0 retc, output = _get_push_output(tempdir_factory) @@ -631,7 +644,7 @@ def test_pre_push_legacy(tempdir_factory, store): def test_commit_msg_integration_failing( commit_msg_repo, tempdir_factory, store, ): - install(C.CONFIG_FILE, store, hook_type='commit-msg') + install(C.CONFIG_FILE, store, hook_types=['commit-msg']) retc, out = _get_commit_output(tempdir_factory) assert retc == 1 assert out.startswith('Must have "Signed off by:"...') @@ -641,7 +654,7 @@ def test_commit_msg_integration_failing( def test_commit_msg_integration_passing( commit_msg_repo, tempdir_factory, store, ): - install(C.CONFIG_FILE, store, hook_type='commit-msg') + install(C.CONFIG_FILE, store, hook_types=['commit-msg']) msg = 'Hi\nSigned off by: me, lol' retc, out = _get_commit_output(tempdir_factory, msg=msg) assert retc == 0 @@ -662,7 +675,7 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): ) make_executable(hook_path) - install(C.CONFIG_FILE, store, hook_type='commit-msg') + install(C.CONFIG_FILE, store, hook_types=['commit-msg']) msg = 'Hi\nSigned off by: asottile' retc, out = _get_commit_output(tempdir_factory, msg=msg) @@ -675,7 +688,7 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): def test_prepare_commit_msg_integration_failing( failing_prepare_commit_msg_repo, tempdir_factory, store, ): - install(C.CONFIG_FILE, store, hook_type='prepare-commit-msg') + install(C.CONFIG_FILE, store, hook_types=['prepare-commit-msg']) retc, out = _get_commit_output(tempdir_factory) assert retc == 1 assert out.startswith('Add "Signed off by:"...') @@ -685,7 +698,7 @@ def test_prepare_commit_msg_integration_failing( def test_prepare_commit_msg_integration_passing( prepare_commit_msg_repo, tempdir_factory, store, ): - install(C.CONFIG_FILE, store, hook_type='prepare-commit-msg') + install(C.CONFIG_FILE, store, hook_types=['prepare-commit-msg']) msg = 'Hi' retc, out = _get_commit_output(tempdir_factory, msg=msg) assert retc == 0 @@ -715,7 +728,7 @@ def test_prepare_commit_msg_legacy( ) make_executable(hook_path) - install(C.CONFIG_FILE, store, hook_type='prepare-commit-msg') + install(C.CONFIG_FILE, store, hook_types=['prepare-commit-msg']) msg = 'Hi' retc, out = _get_commit_output(tempdir_factory, msg=msg) @@ -735,7 +748,8 @@ def test_install_disallow_missing_config(tempdir_factory, store): with cwd(path): remove_config_from_repo(path) ret = install( - C.CONFIG_FILE, store, overwrite=True, skip_on_missing_config=False, + C.CONFIG_FILE, store, hook_types=['pre-commit'], + overwrite=True, skip_on_missing_config=False, ) assert ret == 0 @@ -748,7 +762,8 @@ def test_install_allow_missing_config(tempdir_factory, store): with cwd(path): remove_config_from_repo(path) ret = install( - C.CONFIG_FILE, store, overwrite=True, skip_on_missing_config=True, + C.CONFIG_FILE, store, hook_types=['pre-commit'], + overwrite=True, skip_on_missing_config=True, ) assert ret == 0 @@ -766,7 +781,8 @@ def test_install_temporarily_allow_mising_config(tempdir_factory, store): with cwd(path): remove_config_from_repo(path) ret = install( - C.CONFIG_FILE, store, overwrite=True, skip_on_missing_config=False, + C.CONFIG_FILE, store, hook_types=['pre-commit'], + overwrite=True, skip_on_missing_config=False, ) assert ret == 0 diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 49ce008c4..f6d5c93f5 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -525,7 +525,7 @@ def test_stdout_write_bug_py26(repo_with_failing_hook, store, tempdir_factory): config['repos'][0]['hooks'][0]['args'] = ['☃'] stage_a_file() - install(C.CONFIG_FILE, store) + install(C.CONFIG_FILE, store, hook_types=['pre-commit']) # Have to use subprocess because pytest monkeypatches sys.stdout _, stdout, _ = git_commit( @@ -555,7 +555,7 @@ def test_lots_of_files(store, tempdir_factory): open(filename, 'w').close() cmd_output('git', 'add', '.') - install(C.CONFIG_FILE, store) + install(C.CONFIG_FILE, store, hook_types=['pre-commit']) git_commit( fn=cmd_output_mocked_pre_commit_home, From de63b6a8508ec10f89ec898abc27a915ca268479 Mon Sep 17 00:00:00 2001 From: marqueewinq Date: Tue, 24 Sep 2019 13:34:46 +0300 Subject: [PATCH 471/544] updated import style; put the version info on top of error message; fixed tests --- pre_commit/error_handler.py | 10 ++++++---- tests/error_handler_test.py | 32 +++++++++++++++++--------------- tests/main_test.py | 7 +++---- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 3f5cfe209..6b6f8edfc 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -9,9 +9,9 @@ import six +import pre_commit.constants as C from pre_commit import five from pre_commit import output -from pre_commit.constants import VERSION as pre_commit_version from pre_commit.store import Store @@ -28,12 +28,14 @@ def _to_bytes(exc): def _log_and_exit(msg, exc, formatted): error_msg = b''.join(( + _to_bytes('### version information\n'), + _to_bytes('pre-commit.version={}\n'.format(C.VERSION)), + _to_bytes('sys.version={}\n'.format(sys.version.replace('\n', ' '))), + _to_bytes('sys.executable={}\n'.format(sys.executable)), + _to_bytes('### error information\n'), five.to_bytes(msg), b': ', five.to_bytes(type(exc).__name__), b': ', _to_bytes(exc), b'\n', - _to_bytes('pre-commit.version={}\n'.format(pre_commit_version)), - _to_bytes('sys.version={}\n'.format(sys.version.replace('\n', ' '))), - _to_bytes('sys.executable={}\n'.format(sys.executable)), )) output.write(error_msg) store = Store() diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 244859cfe..e68209360 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -104,29 +104,30 @@ def test_log_and_exit(cap_out, mock_store_dir): printed = cap_out.get() log_file = os.path.join(mock_store_dir, 'pre-commit.log') - printed_lines = printed.split('\n') - assert len(printed_lines) == 6, printed_lines - assert printed_lines[0] == 'msg: FatalError: hai' + printed_lines = printed.splitlines() + print(printed_lines) + assert len(printed_lines) == 7 + assert printed_lines[0] == '### version information' assert re.match(r'^pre-commit.version=\d+\.\d+\.\d+$', printed_lines[1]) assert printed_lines[2].startswith('sys.version=') assert printed_lines[3].startswith('sys.executable=') - assert printed_lines[4] == 'Check the log at {}'.format(log_file) - assert printed_lines[5] == '' # checks for \n at the end of last line + assert printed_lines[4] == '### error information' + assert printed_lines[5] == 'msg: FatalError: hai' + assert printed_lines[6] == 'Check the log at {}'.format(log_file) assert os.path.exists(log_file) with io.open(log_file) as f: - logged_lines = f.read().split('\n') - assert len(logged_lines) == 6, logged_lines - assert logged_lines[0] == 'msg: FatalError: hai' + logged_lines = f.read().splitlines() + assert len(logged_lines) == 7 + assert printed_lines[0] == '### version information' assert re.match( r'^pre-commit.version=\d+\.\d+\.\d+$', printed_lines[1], ) assert logged_lines[2].startswith('sys.version=') assert logged_lines[3].startswith('sys.executable=') - assert logged_lines[4] == "I'm a stacktrace" - # checks for \n at the end of stack trace - assert printed_lines[5] == '' + assert logged_lines[5] == 'msg: FatalError: hai' + assert logged_lines[6] == "I'm a stacktrace" def test_error_handler_non_ascii_exception(mock_store_dir): @@ -148,7 +149,8 @@ def test_error_handler_no_tty(tempdir_factory): pre_commit_home=pre_commit_home, ) log_file = os.path.join(pre_commit_home, 'pre-commit.log') - output_lines = output[1].replace('\r', '').split('\n') - assert output_lines[0] == 'An unexpected error has occurred: ValueError: ☃' - assert output_lines[-2] == 'Check the log at {}'.format(log_file) - assert output_lines[-1] == '' # checks for \n at the end of stack trace + output_lines = output[1].replace('\r', '').splitlines() + assert ( + output_lines[-2] == 'An unexpected error has occurred: ValueError: ☃' + ) + assert output_lines[-1] == 'Check the log at {}'.format(log_file) diff --git a/tests/main_test.py b/tests/main_test.py index 7ebd0ef44..aad9c4b92 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -164,14 +164,13 @@ def test_expected_fatal_error_no_git_repo(in_tmpdir, cap_out, mock_store_dir): with pytest.raises(SystemExit): main.main([]) log_file = os.path.join(mock_store_dir, 'pre-commit.log') - cap_out_lines = cap_out.get().split('\n') + cap_out_lines = cap_out.get().splitlines() assert ( - cap_out_lines[0] == + cap_out_lines[-2] == 'An error has occurred: FatalError: git failed. ' 'Is it installed, and are you in a Git repository directory?' ) - assert cap_out_lines[-2] == 'Check the log at {}'.format(log_file) - assert cap_out_lines[-1] == '' # checks for \n at the end of error message + assert cap_out_lines[-1] == 'Check the log at {}'.format(log_file) def test_warning_on_tags_only(mock_commands, cap_out, mock_store_dir): From e0155fbd6670349c659bfd59efea4f0784034464 Mon Sep 17 00:00:00 2001 From: marqueewinq Date: Tue, 24 Sep 2019 15:50:07 +0300 Subject: [PATCH 472/544] removed meta from stdout; replaced `=` with `: `; handled sys.version newlines; stylized errorlog to md --- pre_commit/error_handler.py | 22 ++++++++++++++----- tests/error_handler_test.py | 43 +++++++++++++++++++------------------ 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 6b6f8edfc..d723aa6ed 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -28,11 +28,6 @@ def _to_bytes(exc): def _log_and_exit(msg, exc, formatted): error_msg = b''.join(( - _to_bytes('### version information\n'), - _to_bytes('pre-commit.version={}\n'.format(C.VERSION)), - _to_bytes('sys.version={}\n'.format(sys.version.replace('\n', ' '))), - _to_bytes('sys.executable={}\n'.format(sys.executable)), - _to_bytes('### error information\n'), five.to_bytes(msg), b': ', five.to_bytes(type(exc).__name__), b': ', _to_bytes(exc), b'\n', @@ -41,9 +36,26 @@ def _log_and_exit(msg, exc, formatted): store = Store() log_path = os.path.join(store.directory, 'pre-commit.log') output.write_line('Check the log at {}'.format(log_path)) + + meta_info_msg = '### version information\n```\n' + meta_info_msg += 'pre-commit.version: {}\n'.format(C.VERSION) + meta_info_msg += 'sys.version: \n{}\n'.format( + '\n'.join( + [ + '\t{}'.format(line) + for line in sys.version.splitlines() + ], + ), + ) + meta_info_msg += 'sys.executable: {}\n'.format(sys.executable) + meta_info_msg += 'os.name: {}\n'.format(os.name) + meta_info_msg += 'sys.platform: {}\n```\n'.format(sys.platform) + meta_info_msg += '### error information\n```\n' with open(log_path, 'wb') as log: + output.write(meta_info_msg, stream=log) output.write(error_msg, stream=log) output.write_line(formatted, stream=log) + output.write('\n```\n', stream=log) raise SystemExit(1) diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index e68209360..99edfdb32 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -104,30 +104,30 @@ def test_log_and_exit(cap_out, mock_store_dir): printed = cap_out.get() log_file = os.path.join(mock_store_dir, 'pre-commit.log') - printed_lines = printed.splitlines() - print(printed_lines) - assert len(printed_lines) == 7 - assert printed_lines[0] == '### version information' - assert re.match(r'^pre-commit.version=\d+\.\d+\.\d+$', printed_lines[1]) - assert printed_lines[2].startswith('sys.version=') - assert printed_lines[3].startswith('sys.executable=') - assert printed_lines[4] == '### error information' - assert printed_lines[5] == 'msg: FatalError: hai' - assert printed_lines[6] == 'Check the log at {}'.format(log_file) + assert printed == ( + 'msg: FatalError: hai\n' 'Check the log at {}\n'.format(log_file) + ) assert os.path.exists(log_file) with io.open(log_file) as f: - logged_lines = f.read().splitlines() - assert len(logged_lines) == 7 - assert printed_lines[0] == '### version information' - assert re.match( - r'^pre-commit.version=\d+\.\d+\.\d+$', - printed_lines[1], + logged = f.read() + expected = ( + r'^### version information\n' + r'```\n' + r'pre-commit.version: \d+\.\d+\.\d+\n' + r'sys.version: (.*\n)*' + r'sys.executable: .*\n' + r'os.name: .*\n' + r'sys.platform: .*\n' + r'```\n' + r'### error information\n' + r'```\n' + r'msg: FatalError: hai\n' + r"I'm a stacktrace\n" + r'\n' + r'```\n' ) - assert logged_lines[2].startswith('sys.version=') - assert logged_lines[3].startswith('sys.executable=') - assert logged_lines[5] == 'msg: FatalError: hai' - assert logged_lines[6] == "I'm a stacktrace" + assert re.match(expected, logged) def test_error_handler_non_ascii_exception(mock_store_dir): @@ -139,7 +139,8 @@ def test_error_handler_non_ascii_exception(mock_store_dir): def test_error_handler_no_tty(tempdir_factory): pre_commit_home = tempdir_factory.get() output = cmd_output_mocked_pre_commit_home( - sys.executable, '-c', + sys.executable, + '-c', 'from __future__ import unicode_literals\n' 'from pre_commit.error_handler import error_handler\n' 'with error_handler():\n' From cb164ef629b5dff9edf35f09233490b671243547 Mon Sep 17 00:00:00 2001 From: marqueewinq Date: Tue, 24 Sep 2019 16:25:27 +0300 Subject: [PATCH 473/544] replaced str concat with .write_line(); replaced \t with spaces; removed trailing space in logs --- pre_commit/error_handler.py | 40 +++++++++++++++++++++++-------------- tests/error_handler_test.py | 2 +- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index d723aa6ed..5b09a0179 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -37,22 +37,32 @@ def _log_and_exit(msg, exc, formatted): log_path = os.path.join(store.directory, 'pre-commit.log') output.write_line('Check the log at {}'.format(log_path)) - meta_info_msg = '### version information\n```\n' - meta_info_msg += 'pre-commit.version: {}\n'.format(C.VERSION) - meta_info_msg += 'sys.version: \n{}\n'.format( - '\n'.join( - [ - '\t{}'.format(line) - for line in sys.version.splitlines() - ], - ), - ) - meta_info_msg += 'sys.executable: {}\n'.format(sys.executable) - meta_info_msg += 'os.name: {}\n'.format(os.name) - meta_info_msg += 'sys.platform: {}\n```\n'.format(sys.platform) - meta_info_msg += '### error information\n```\n' with open(log_path, 'wb') as log: - output.write(meta_info_msg, stream=log) + output.write_line( + '### version information\n```', stream=log, + ) + output.write_line( + 'pre-commit.version: {}'.format(C.VERSION), stream=log, + ) + output.write_line( + 'sys.version:\n{}'.format( + '\n'.join( + [ + ' {}'.format(line) + for line in sys.version.splitlines() + ], + ), + ), + stream=log, + ) + output.write_line( + 'sys.executable: {}'.format(sys.executable), stream=log, + ) + output.write_line('os.name: {}'.format(os.name), stream=log) + output.write_line( + 'sys.platform: {}\n```'.format(sys.platform), stream=log, + ) + output.write_line('### error information\n```', stream=log) output.write(error_msg, stream=log) output.write_line(formatted, stream=log) output.write('\n```\n', stream=log) diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 99edfdb32..e94b32065 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -115,7 +115,7 @@ def test_log_and_exit(cap_out, mock_store_dir): r'^### version information\n' r'```\n' r'pre-commit.version: \d+\.\d+\.\d+\n' - r'sys.version: (.*\n)*' + r'sys.version:\n( .*\n)*' r'sys.executable: .*\n' r'os.name: .*\n' r'sys.platform: .*\n' From 795506a486178fee890cd254045bd040144093f6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 24 Sep 2019 09:32:10 -0700 Subject: [PATCH 474/544] Fix up some newlines in output --- pre_commit/error_handler.py | 57 ++++++++++++++++++------------------- pre_commit/util.py | 2 +- tests/error_handler_test.py | 12 ++++++-- tests/util_test.py | 4 +-- 4 files changed, 39 insertions(+), 36 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 5b09a0179..0fa87686d 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -30,42 +30,39 @@ def _log_and_exit(msg, exc, formatted): error_msg = b''.join(( five.to_bytes(msg), b': ', five.to_bytes(type(exc).__name__), b': ', - _to_bytes(exc), b'\n', + _to_bytes(exc), )) - output.write(error_msg) + output.write_line(error_msg) store = Store() log_path = os.path.join(store.directory, 'pre-commit.log') output.write_line('Check the log at {}'.format(log_path)) with open(log_path, 'wb') as log: - output.write_line( - '### version information\n```', stream=log, - ) - output.write_line( - 'pre-commit.version: {}'.format(C.VERSION), stream=log, - ) - output.write_line( - 'sys.version:\n{}'.format( - '\n'.join( - [ - ' {}'.format(line) - for line in sys.version.splitlines() - ], - ), - ), - stream=log, - ) - output.write_line( - 'sys.executable: {}'.format(sys.executable), stream=log, - ) - output.write_line('os.name: {}'.format(os.name), stream=log) - output.write_line( - 'sys.platform: {}\n```'.format(sys.platform), stream=log, - ) - output.write_line('### error information\n```', stream=log) - output.write(error_msg, stream=log) - output.write_line(formatted, stream=log) - output.write('\n```\n', stream=log) + def _log_line(*s): # type: (*str) -> None + output.write_line(*s, stream=log) + + _log_line('### version information') + _log_line() + _log_line('```') + _log_line('pre-commit version: {}'.format(C.VERSION)) + _log_line('sys.version:') + for line in sys.version.splitlines(): + _log_line(' {}'.format(line)) + _log_line('sys.executable: {}'.format(sys.executable)) + _log_line('os.name: {}'.format(os.name)) + _log_line('sys.platform: {}'.format(sys.platform)) + _log_line('```') + _log_line() + + _log_line('### error information') + _log_line() + _log_line('```') + _log_line(error_msg) + _log_line('```') + _log_line() + _log_line('```') + _log_line(formatted) + _log_line('```') raise SystemExit(1) diff --git a/pre_commit/util.py b/pre_commit/util.py index eb5411fdd..5aee0b08a 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -103,7 +103,7 @@ def to_bytes(self): ), ), b'Output: ', output[0], b'\n', - b'Errors: ', output[1], b'\n', + b'Errors: ', output[1], )) def to_text(self): diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index e94b32065..ff311a24d 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -113,19 +113,25 @@ def test_log_and_exit(cap_out, mock_store_dir): logged = f.read() expected = ( r'^### version information\n' + r'\n' r'```\n' - r'pre-commit.version: \d+\.\d+\.\d+\n' - r'sys.version:\n( .*\n)*' + r'pre-commit version: \d+\.\d+\.\d+\n' + r'sys.version:\n' + r'( .*\n)*' r'sys.executable: .*\n' r'os.name: .*\n' r'sys.platform: .*\n' r'```\n' + r'\n' r'### error information\n' + r'\n' r'```\n' r'msg: FatalError: hai\n' - r"I'm a stacktrace\n" + r'```\n' r'\n' r'```\n' + r"I'm a stacktrace\n" + r'```\n' ) assert re.match(expected, logged) diff --git a/tests/util_test.py b/tests/util_test.py index c9838c555..867969c34 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -24,7 +24,7 @@ def test_CalledProcessError_str(): 'Output: \n' ' stdout\n' 'Errors: \n' - ' stderr\n' + ' stderr' ) @@ -37,7 +37,7 @@ def test_CalledProcessError_str_nooutput(): 'Return code: 1\n' 'Expected return code: 0\n' 'Output: (none)\n' - 'Errors: (none)\n' + 'Errors: (none)' ) From 36609ee305278ba8c923e2c83c4c548816ee13de Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Sep 2019 10:29:53 -0700 Subject: [PATCH 475/544] Fix hook_types when calling init-templatedir --- pre_commit/main.py | 20 ++++++++++++++------ tests/main_test.py | 14 ++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 8d2d63020..59de5f24c 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -55,12 +55,25 @@ def _add_config_option(parser): ) +class AppendReplaceDefault(argparse.Action): + def __init__(self, *args, **kwargs): + super(AppendReplaceDefault, self).__init__(*args, **kwargs) + self.appended = False + + def __call__(self, parser, namespace, values, option_string=None): + if not self.appended: + setattr(namespace, self.dest, []) + self.appended = True + getattr(namespace, self.dest).append(values) + + def _add_hook_type_option(parser): parser.add_argument( '-t', '--hook-type', choices=( 'pre-commit', 'pre-push', 'prepare-commit-msg', 'commit-msg', ), - action='append', + action=AppendReplaceDefault, + default=['pre-commit'], dest='hook_types', ) @@ -121,11 +134,6 @@ def _adjust_args_and_chdir(args): args.files = [os.path.relpath(filename) for filename in args.files] if args.command == 'try-repo' and os.path.exists(args.repo): args.repo = os.path.relpath(args.repo) - if ( - args.command in {'install', 'uninstall', 'init-templatedir'} and - not args.hook_types - ): - args.hook_types = ['pre-commit'] def main(argv=None): diff --git a/tests/main_test.py b/tests/main_test.py index aad9c4b92..364e0d390 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -13,6 +13,20 @@ from testing.auto_namedtuple import auto_namedtuple +@pytest.mark.parametrize( + ('argv', 'expected'), + ( + ((), ['f']), + (('--f', 'x'), ['x']), + (('--f', 'x', '--f', 'y'), ['x', 'y']), + ), +) +def test_append_replace_default(argv, expected): + parser = argparse.ArgumentParser() + parser.add_argument('--f', action=main.AppendReplaceDefault, default=['f']) + assert parser.parse_args(argv).f == expected + + class Args(object): def __init__(self, **kwargs): kwargs.setdefault('command', 'help') From f612aeb22baee1c3a40615a36614d342c27dcd17 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 6 Oct 2019 15:16:47 -0700 Subject: [PATCH 476/544] Split out cmd_output_b --- pre_commit/commands/autoupdate.py | 3 ++- pre_commit/commands/install_uninstall.py | 3 +-- pre_commit/commands/run.py | 14 +++++--------- pre_commit/commands/try_repo.py | 8 ++++---- pre_commit/git.py | 22 ++++++++++++++-------- pre_commit/languages/docker.py | 6 ++++-- pre_commit/languages/golang.py | 5 +++-- pre_commit/languages/helpers.py | 4 ++-- pre_commit/languages/node.py | 3 ++- pre_commit/languages/python.py | 6 +++--- pre_commit/languages/python_venv.py | 3 ++- pre_commit/languages/rust.py | 4 ++-- pre_commit/languages/swift.py | 4 ++-- pre_commit/make_archives.py | 6 +++--- pre_commit/staged_files_only.py | 12 ++++++------ pre_commit/store.py | 6 +++--- pre_commit/util.py | 22 ++++++++++++---------- pre_commit/xargs.py | 4 ++-- tests/languages/docker_test.py | 2 +- tests/repository_test.py | 12 ++++++------ 20 files changed, 79 insertions(+), 70 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index fdada1858..d56a88fb3 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -21,6 +21,7 @@ from pre_commit.commands.migrate_config import migrate_config from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b from pre_commit.util import tmpdir @@ -38,7 +39,7 @@ def _update_repo(repo_config, store, tags_only): """ with tmpdir() as repo_path: git.init_repo(repo_path, repo_config['repo']) - cmd_output('git', 'fetch', 'origin', 'HEAD', '--tags', cwd=repo_path) + cmd_output_b('git', 'fetch', 'origin', 'HEAD', '--tags', cwd=repo_path) tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags') if tags_only: diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 0fda6272a..d6d7ac934 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -13,7 +13,6 @@ from pre_commit.clientlib import load_config from pre_commit.repository import all_hooks from pre_commit.repository import install_hook_envs -from pre_commit.util import cmd_output from pre_commit.util import make_executable from pre_commit.util import mkdirp from pre_commit.util import resource_text @@ -117,7 +116,7 @@ def install( overwrite=False, hooks=False, skip_on_missing_config=False, git_dir=None, ): - if cmd_output('git', 'config', 'core.hooksPath', retcode=None)[1].strip(): + if git.has_core_hookpaths_set(): logger.error( 'Cowardly refusing to install hooks with `core.hooksPath` set.\n' 'hint: `git config --unset-all core.hooksPath`', diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 4087a6505..aee3d9c23 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -16,7 +16,7 @@ from pre_commit.repository import all_hooks from pre_commit.repository import install_hook_envs from pre_commit.staged_files_only import staged_files_only -from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b from pre_commit.util import noop_context @@ -117,15 +117,11 @@ def _run_single_hook(classifier, hook, args, skips, cols): ) sys.stdout.flush() - diff_before = cmd_output( - 'git', 'diff', '--no-ext-diff', retcode=None, encoding=None, - ) + diff_before = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None) retcode, stdout, stderr = hook.run( tuple(filenames) if hook.pass_filenames else (), ) - diff_after = cmd_output( - 'git', 'diff', '--no-ext-diff', retcode=None, encoding=None, - ) + diff_after = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None) file_modifications = diff_before != diff_after @@ -235,12 +231,12 @@ def _run_hooks(config, hooks, args, environ): def _has_unmerged_paths(): - _, stdout, _ = cmd_output('git', 'ls-files', '--unmerged') + _, stdout, _ = cmd_output_b('git', 'ls-files', '--unmerged') return bool(stdout.strip()) def _has_unstaged_config(config_file): - retcode, _, _ = cmd_output( + retcode, _, _ = cmd_output_b( 'git', 'diff', '--no-ext-diff', '--exit-code', config_file, retcode=None, ) diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index 3e256ad89..b7b0c990b 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -13,7 +13,7 @@ from pre_commit.clientlib import load_manifest from pre_commit.commands.run import run from pre_commit.store import Store -from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b from pre_commit.util import tmpdir from pre_commit.xargs import xargs @@ -31,8 +31,8 @@ def _repo_ref(tmpdir, repo, ref): logger.warning('Creating temporary repo with uncommitted changes...') shadow = os.path.join(tmpdir, 'shadow-repo') - cmd_output('git', 'clone', repo, shadow) - cmd_output('git', 'checkout', ref, '-b', '_pc_tmp', cwd=shadow) + cmd_output_b('git', 'clone', repo, shadow) + cmd_output_b('git', 'checkout', ref, '-b', '_pc_tmp', cwd=shadow) idx = git.git_path('index', repo=shadow) objs = git.git_path('objects', repo=shadow) @@ -42,7 +42,7 @@ def _repo_ref(tmpdir, repo, ref): if staged_files: xargs(('git', 'add', '--'), staged_files, cwd=repo, env=env) - cmd_output('git', 'add', '-u', cwd=repo, env=env) + cmd_output_b('git', 'add', '-u', cwd=repo, env=env) git.commit(repo=shadow) return shadow, git.head_rev(shadow) diff --git a/pre_commit/git.py b/pre_commit/git.py index c51930e7d..3ee9ca3af 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -5,6 +5,7 @@ import sys from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b logger = logging.getLogger(__name__) @@ -50,8 +51,8 @@ def get_git_dir(git_root='.'): def get_remote_url(git_root): - ret = cmd_output('git', 'config', 'remote.origin.url', cwd=git_root)[1] - return ret.strip() + _, out, _ = cmd_output('git', 'config', 'remote.origin.url', cwd=git_root) + return out.strip() def is_in_merge_conflict(): @@ -105,8 +106,8 @@ def get_staged_files(cwd=None): def intent_to_add_files(): - _, stdout_binary, _ = cmd_output('git', 'status', '--porcelain', '-z') - parts = list(reversed(zsplit(stdout_binary))) + _, stdout, _ = cmd_output('git', 'status', '--porcelain', '-z') + parts = list(reversed(zsplit(stdout))) intent_to_add = [] while parts: line = parts.pop() @@ -140,7 +141,12 @@ def has_diff(*args, **kwargs): repo = kwargs.pop('repo', '.') assert not kwargs, kwargs cmd = ('git', 'diff', '--quiet', '--no-ext-diff') + args - return cmd_output(*cmd, cwd=repo, retcode=None)[0] + return cmd_output_b(*cmd, cwd=repo, retcode=None)[0] + + +def has_core_hookpaths_set(): + _, out, _ = cmd_output_b('git', 'config', 'core.hooksPath', retcode=None) + return bool(out.strip()) def init_repo(path, remote): @@ -148,8 +154,8 @@ def init_repo(path, remote): remote = os.path.abspath(remote) env = no_git_env() - cmd_output('git', 'init', path, env=env) - cmd_output('git', 'remote', 'add', 'origin', remote, cwd=path, env=env) + cmd_output_b('git', 'init', path, env=env) + cmd_output_b('git', 'remote', 'add', 'origin', remote, cwd=path, env=env) def commit(repo='.'): @@ -158,7 +164,7 @@ def commit(repo='.'): env['GIT_AUTHOR_NAME'] = env['GIT_COMMITTER_NAME'] = name env['GIT_AUTHOR_EMAIL'] = env['GIT_COMMITTER_EMAIL'] = email cmd = ('git', 'commit', '--no-edit', '--no-gpg-sign', '-n', '-minit') - cmd_output(*cmd, cwd=repo, env=env) + cmd_output_b(*cmd, cwd=repo, env=env) def git_path(name, repo='.'): diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 4517050be..b7a4e3223 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -9,7 +9,7 @@ from pre_commit.languages import helpers from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure -from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'docker' @@ -29,9 +29,11 @@ def docker_tag(prefix): # pragma: windows no cover def docker_is_running(): # pragma: windows no cover try: - return cmd_output('docker', 'ps')[0] == 0 + cmd_output_b('docker', 'ps') except CalledProcessError: return False + else: + return True def assert_docker_available(): # pragma: windows no cover diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index f6124dd5d..57984c5c5 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -11,6 +11,7 @@ from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b from pre_commit.util import rmtree @@ -70,9 +71,9 @@ def install_environment(prefix, version, additional_dependencies): gopath = directory env = dict(os.environ, GOPATH=gopath) env.pop('GOBIN', None) - cmd_output('go', 'get', './...', cwd=repo_src_dir, env=env) + cmd_output_b('go', 'get', './...', cwd=repo_src_dir, env=env) for dependency in additional_dependencies: - cmd_output('go', 'get', dependency, cwd=repo_src_dir, env=env) + cmd_output_b('go', 'get', dependency, cwd=repo_src_dir, env=env) # Same some disk space, we don't need these after installation rmtree(prefix.path(directory, 'src')) pkgdir = prefix.path(directory, 'pkg') diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 0915f4105..8a38dec94 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -8,14 +8,14 @@ import six import pre_commit.constants as C -from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b from pre_commit.xargs import xargs FIXED_RANDOM_SEED = 1542676186 def run_setup_cmd(prefix, cmd): - cmd_output(*cmd, cwd=prefix.prefix_dir, encoding=None) + cmd_output_b(*cmd, cwd=prefix.prefix_dir) def environment_dir(ENVIRONMENT_DIR, language_version): diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 7d85a327b..1cb947a04 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -11,6 +11,7 @@ from pre_commit.languages.python import bin_dir from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'node_env' @@ -65,7 +66,7 @@ def install_environment( ] if version != C.DEFAULT: cmd.extend(['-n', version]) - cmd_output(*cmd) + cmd_output_b(*cmd) with in_env(prefix, version): # https://npm.community/t/npm-install-g-git-vs-git-clone-cd-npm-install-g/5449 diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 6d125a439..948b28973 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -13,6 +13,7 @@ from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'py_env' @@ -143,11 +144,10 @@ def in_env(prefix, language_version): def healthy(prefix, language_version): with in_env(prefix, language_version): - retcode, _, _ = cmd_output( + retcode, _, _ = cmd_output_b( 'python', '-c', 'import ctypes, datetime, io, os, ssl, weakref', retcode=None, - encoding=None, ) return retcode == 0 @@ -177,7 +177,7 @@ def install_environment(prefix, version, additional_dependencies): def make_venv(envdir, python): env = dict(os.environ, VIRTUALENV_NO_DOWNLOAD='1') cmd = (sys.executable, '-mvirtualenv', envdir, '-p', python) - cmd_output(*cmd, env=env, cwd='/') + cmd_output_b(*cmd, env=env, cwd='/') _interface = py_interface(ENVIRONMENT_DIR, make_venv) diff --git a/pre_commit/languages/python_venv.py b/pre_commit/languages/python_venv.py index b7658f5d8..ef9043fc6 100644 --- a/pre_commit/languages/python_venv.py +++ b/pre_commit/languages/python_venv.py @@ -6,6 +6,7 @@ from pre_commit.languages import python from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'py_venv' @@ -48,7 +49,7 @@ def orig_py_exe(exe): # pragma: no cover (platform specific) def make_venv(envdir, python): - cmd_output(orig_py_exe(python), '-mvenv', envdir, cwd='/') + cmd_output_b(orig_py_exe(python), '-mvenv', envdir, cwd='/') _interface = python.py_interface(ENVIRONMENT_DIR, make_venv) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 4b25a9d10..9885c3c4d 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -10,7 +10,7 @@ from pre_commit.envcontext import Var from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure -from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'rustenv' @@ -83,7 +83,7 @@ def install_environment(prefix, version, additional_dependencies): packages_to_install.add((package,)) for package in packages_to_install: - cmd_output( + cmd_output_b( 'cargo', 'install', '--bins', '--root', directory, *package, cwd=prefix.prefix_dir ) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 3f5a92f14..9e1bf62f7 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -8,7 +8,7 @@ from pre_commit.envcontext import Var from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure -from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'swift_env' get_default_version = helpers.basic_get_default_version @@ -43,7 +43,7 @@ def install_environment( # Build the swift package with clean_path_on_failure(directory): os.mkdir(directory) - cmd_output( + cmd_output_b( 'swift', 'build', '-C', prefix.prefix_dir, '-c', BUILD_CONFIG, diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index 9dd9e5e77..cff45d0cd 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -7,7 +7,7 @@ import tarfile from pre_commit import output -from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b from pre_commit.util import rmtree from pre_commit.util import tmpdir @@ -39,8 +39,8 @@ def make_archive(name, repo, ref, destdir): output_path = os.path.join(destdir, name + '.tar.gz') with tmpdir() as tempdir: # Clone the repository to the temporary directory - cmd_output('git', 'clone', repo, tempdir) - cmd_output('git', 'checkout', ref, cwd=tempdir) + cmd_output_b('git', 'clone', repo, tempdir) + cmd_output_b('git', 'checkout', ref, cwd=tempdir) # We don't want the '.git' directory # It adds a bunch of size to the archive and we don't use it at diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 7af319d72..5bb841547 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -9,6 +9,7 @@ from pre_commit import git from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b from pre_commit.util import mkdirp from pre_commit.xargs import xargs @@ -19,10 +20,10 @@ def _git_apply(patch): args = ('apply', '--whitespace=nowarn', patch) try: - cmd_output('git', *args, encoding=None) + cmd_output_b('git', *args) except CalledProcessError: # Retry with autocrlf=false -- see #570 - cmd_output('git', '-c', 'core.autocrlf=false', *args, encoding=None) + cmd_output_b('git', '-c', 'core.autocrlf=false', *args) @contextlib.contextmanager @@ -43,11 +44,10 @@ def _intent_to_add_cleared(): @contextlib.contextmanager def _unstaged_changes_cleared(patch_dir): tree = cmd_output('git', 'write-tree')[1].strip() - retcode, diff_stdout_binary, _ = cmd_output( + retcode, diff_stdout_binary, _ = cmd_output_b( 'git', 'diff-index', '--ignore-submodules', '--binary', '--exit-code', '--no-color', '--no-ext-diff', tree, '--', retcode=None, - encoding=None, ) if retcode and diff_stdout_binary.strip(): patch_filename = 'patch{}'.format(int(time.time())) @@ -62,7 +62,7 @@ def _unstaged_changes_cleared(patch_dir): patch_file.write(diff_stdout_binary) # Clear the working directory of unstaged changes - cmd_output('git', 'checkout', '--', '.') + cmd_output_b('git', 'checkout', '--', '.') try: yield finally: @@ -77,7 +77,7 @@ def _unstaged_changes_cleared(patch_dir): # We failed to apply the patch, presumably due to fixes made # by hooks. # Roll back the changes made by hooks. - cmd_output('git', 'checkout', '--', '.') + cmd_output_b('git', 'checkout', '--', '.') _git_apply(patch_filename) logger.info('Restored changes from {}.'.format(patch_filename)) else: diff --git a/pre_commit/store.py b/pre_commit/store.py index 55c57a3e6..5215d80a7 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -12,7 +12,7 @@ from pre_commit import git from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure -from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b from pre_commit.util import resource_text from pre_commit.util import rmtree @@ -161,7 +161,7 @@ def clone_strategy(directory): env = git.no_git_env() def _git_cmd(*args): - cmd_output('git', *args, cwd=directory, env=env) + cmd_output_b('git', *args, cwd=directory, env=env) try: self._shallow_clone(ref, _git_cmd) @@ -186,7 +186,7 @@ def make_local_strategy(directory): # initialize the git repository so it looks more like cloned repos def _git_cmd(*args): - cmd_output('git', *args, cwd=directory, env=env) + cmd_output_b('git', *args, cwd=directory, env=env) git.init_repo(directory, '<>') _git_cmd('add', '.') diff --git a/pre_commit/util.py b/pre_commit/util.py index 5aee0b08a..1a93a2333 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -117,9 +117,8 @@ def to_text(self): __str__ = to_text -def cmd_output(*cmd, **kwargs): +def cmd_output_b(*cmd, **kwargs): retcode = kwargs.pop('retcode', 0) - encoding = kwargs.pop('encoding', 'UTF-8') popen_kwargs = { 'stdin': subprocess.PIPE, @@ -133,26 +132,29 @@ def cmd_output(*cmd, **kwargs): five.n(key): five.n(value) for key, value in kwargs.pop('env', {}).items() } or None + popen_kwargs.update(kwargs) try: cmd = parse_shebang.normalize_cmd(cmd) except parse_shebang.ExecutableNotFoundError as e: - returncode, stdout, stderr = e.to_output() + returncode, stdout_b, stderr_b = e.to_output() else: - popen_kwargs.update(kwargs) proc = subprocess.Popen(cmd, **popen_kwargs) - stdout, stderr = proc.communicate() + stdout_b, stderr_b = proc.communicate() returncode = proc.returncode - if encoding is not None and stdout is not None: - stdout = stdout.decode(encoding) - if encoding is not None and stderr is not None: - stderr = stderr.decode(encoding) if retcode is not None and retcode != returncode: raise CalledProcessError( - returncode, cmd, retcode, output=(stdout, stderr), + returncode, cmd, retcode, output=(stdout_b, stderr_b), ) + return returncode, stdout_b, stderr_b + + +def cmd_output(*cmd, **kwargs): + returncode, stdout_b, stderr_b = cmd_output_b(*cmd, **kwargs) + stdout = stdout_b.decode('UTF-8') if stdout_b is not None else None + stderr = stderr_b.decode('UTF-8') if stderr_b is not None else None return returncode, stdout, stderr diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 936a5beff..332681d8b 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -11,7 +11,7 @@ import six from pre_commit import parse_shebang -from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b def _environ_size(_env=None): @@ -122,7 +122,7 @@ def xargs(cmd, varargs, **kwargs): partitions = partition(cmd, varargs, target_concurrency, max_length) def run_cmd_partition(run_cmd): - return cmd_output(*run_cmd, encoding=None, retcode=None, **kwargs) + return cmd_output_b(*run_cmd, retcode=None, **kwargs) threads = min(len(partitions), target_concurrency) with _thread_mapper(threads) as thread_map: diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 1a96e69dc..42616cdc5 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -9,7 +9,7 @@ def test_docker_is_running_process_error(): with mock.patch( - 'pre_commit.languages.docker.cmd_output', + 'pre_commit.languages.docker.cmd_output_b', side_effect=CalledProcessError(*(None,) * 4), ): assert docker.docker_is_running() is False diff --git a/tests/repository_test.py b/tests/repository_test.py index 03ffeb07c..ec09da36c 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -28,6 +28,7 @@ from pre_commit.repository import Hook from pre_commit.repository import install_hook_envs from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo from testing.fixtures import modify_manifest @@ -380,7 +381,7 @@ def _make_grep_repo(language, entry, store, args=()): @pytest.fixture def greppable_files(tmpdir): with tmpdir.as_cwd(): - cmd_output('git', 'init', '.') + cmd_output_b('git', 'init', '.') tmpdir.join('f1').write_binary(b"hello'hi\nworld\n") tmpdir.join('f2').write_binary(b'foo\nbar\nbaz\n') tmpdir.join('f3').write_binary(b'[WARN] hi\n') @@ -439,9 +440,8 @@ def no_grep(exe, **kwargs): def _norm_pwd(path): # Under windows bash's temp and windows temp is different. # This normalizes to the bash /tmp - return cmd_output( + return cmd_output_b( 'bash', '-c', "cd '{}' && pwd".format(path), - encoding=None, )[1].strip() @@ -654,7 +654,7 @@ def test_invalidated_virtualenv(tempdir_factory, store): paths = [ os.path.join(libdir, p) for p in ('site.py', 'site.pyc', '__pycache__') ] - cmd_output('rm', '-rf', *paths) + cmd_output_b('rm', '-rf', *paths) # pre-commit should rebuild the virtualenv and it should be runnable retv, stdout, stderr = _get_hook(config, store, 'foo').run(()) @@ -664,7 +664,7 @@ def test_invalidated_virtualenv(tempdir_factory, store): def test_really_long_file_paths(tempdir_factory, store): base_path = tempdir_factory.get() really_long_path = os.path.join(base_path, 'really_long' * 10) - cmd_output('git', 'init', really_long_path) + cmd_output_b('git', 'init', really_long_path) path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) @@ -687,7 +687,7 @@ def test_config_overrides_repo_specifics(tempdir_factory, store): def _create_repo_with_tags(tempdir_factory, src, tag): path = make_repo(tempdir_factory, src) - cmd_output('git', 'tag', tag, cwd=path) + cmd_output_b('git', 'tag', tag, cwd=path) return path From 95dbf1190ae2cb801377abfc18d693ac8db383ed Mon Sep 17 00:00:00 2001 From: WillKoehrsen Date: Mon, 7 Oct 2019 09:27:34 -0400 Subject: [PATCH 477/544] Handle case when executable is not executable - Changed error message if executable is not executable Closes:[1159](https://github.com/pre-commit/pre-commit/issues/1159) --- pre_commit/parse_shebang.py | 6 ++++-- tests/parse_shebang_test.py | 8 ++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index 5a2ba72fc..ab2c9eec6 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -51,10 +51,12 @@ def _error(msg): if exe is None: _error('not found') return exe - elif not os.access(orig, os.X_OK): - _error('not found') elif os.path.isdir(orig): _error('is a directory') + elif not os.path.isfile(orig): + _error('not found') + elif not os.access(orig, os.X_OK): # pragma: windows no cover + _error('is not executable') else: return orig diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index 400a287cb..589533226 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -91,6 +91,14 @@ def test_normexe_does_not_exist_sep(): assert excinfo.value.args == ('Executable `./i-dont-exist-lol` not found',) +@pytest.mark.xfail(os.name == 'nt', reason='posix only',) +def test_normexe_not_executable(tmpdir): # pragma: windows no cover + tmpdir.join('exe').ensure() + with tmpdir.as_cwd(), pytest.raises(OSError) as excinfo: + parse_shebang.normexe('./exe') + assert excinfo.value.args == ('Executable `./exe` is not executable',) + + def test_normexe_is_a_directory(tmpdir): with tmpdir.as_cwd(): tmpdir.join('exe').ensure_dir() From 2633d38a63a923738288fe633b8401649e7e2960 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 12 Oct 2019 13:35:04 -0700 Subject: [PATCH 478/544] Fix ordering of mixed stdout / stderr printing --- pre_commit/commands/run.py | 14 ++++------ pre_commit/xargs.py | 13 +++++----- .../stdout_stderr_repo/.pre-commit-hooks.yaml | 4 +++ testing/resources/stdout_stderr_repo/entry | 13 ++++++++++ tests/repository_test.py | 26 +++++++++++++------ tests/xargs_test.py | 17 ++++++------ 6 files changed, 55 insertions(+), 32 deletions(-) create mode 100644 testing/resources/stdout_stderr_repo/.pre-commit-hooks.yaml create mode 100755 testing/resources/stdout_stderr_repo/entry diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index aee3d9c23..6ab1879d0 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -118,9 +118,7 @@ def _run_single_hook(classifier, hook, args, skips, cols): sys.stdout.flush() diff_before = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None) - retcode, stdout, stderr = hook.run( - tuple(filenames) if hook.pass_filenames else (), - ) + retcode, out = hook.run(tuple(filenames) if hook.pass_filenames else ()) diff_after = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None) file_modifications = diff_before != diff_after @@ -141,7 +139,7 @@ def _run_single_hook(classifier, hook, args, skips, cols): output.write_line(color.format_color(pass_fail, print_color, args.color)) if ( - (stdout or stderr or file_modifications) and + (out or file_modifications) and (retcode or args.verbose or hook.verbose) ): output.write_line('hookid: {}\n'.format(hook.id)) @@ -150,15 +148,13 @@ def _run_single_hook(classifier, hook, args, skips, cols): if file_modifications: output.write('Files were modified by this hook.') - if stdout or stderr: + if out: output.write_line(' Additional output:') output.write_line() - for out in (stdout, stderr): - assert type(out) is bytes, type(out) - if out.strip(): - output.write_line(out.strip(), logfile_name=hook.log_file) + if out.strip(): + output.write_line(out.strip(), logfile_name=hook.log_file) output.write_line() return retcode diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 332681d8b..440317545 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -6,6 +6,7 @@ import contextlib import math import os +import subprocess import sys import six @@ -112,23 +113,24 @@ def xargs(cmd, varargs, **kwargs): max_length = kwargs.pop('_max_length', _get_platform_max_length()) retcode = 0 stdout = b'' - stderr = b'' try: cmd = parse_shebang.normalize_cmd(cmd) except parse_shebang.ExecutableNotFoundError as e: - return e.to_output() + return e.to_output()[:2] partitions = partition(cmd, varargs, target_concurrency, max_length) def run_cmd_partition(run_cmd): - return cmd_output_b(*run_cmd, retcode=None, **kwargs) + return cmd_output_b( + *run_cmd, retcode=None, stderr=subprocess.STDOUT, **kwargs + ) threads = min(len(partitions), target_concurrency) with _thread_mapper(threads) as thread_map: results = thread_map(run_cmd_partition, partitions) - for proc_retcode, proc_out, proc_err in results: + for proc_retcode, proc_out, _ in results: # This is *slightly* too clever so I'll explain it. # First the xor boolean table: # T | F | @@ -141,6 +143,5 @@ def run_cmd_partition(run_cmd): # code. Otherwise, the returncode is unchanged. retcode |= bool(proc_retcode) ^ negate stdout += proc_out - stderr += proc_err - return retcode, stdout, stderr + return retcode, stdout diff --git a/testing/resources/stdout_stderr_repo/.pre-commit-hooks.yaml b/testing/resources/stdout_stderr_repo/.pre-commit-hooks.yaml new file mode 100644 index 000000000..e68174a12 --- /dev/null +++ b/testing/resources/stdout_stderr_repo/.pre-commit-hooks.yaml @@ -0,0 +1,4 @@ +- id: stdout-stderr + name: stdout-stderr + language: script + entry: ./entry diff --git a/testing/resources/stdout_stderr_repo/entry b/testing/resources/stdout_stderr_repo/entry new file mode 100755 index 000000000..e382373dd --- /dev/null +++ b/testing/resources/stdout_stderr_repo/entry @@ -0,0 +1,13 @@ +#!/usr/bin/env python +import sys + + +def main(): + for i in range(6): + f = sys.stdout if i % 2 == 0 else sys.stderr + f.write('{}\n'.format(i)) + f.flush() + + +if __name__ == '__main__': + exit(main()) diff --git a/tests/repository_test.py b/tests/repository_test.py index ec09da36c..344b3a58f 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -177,7 +177,8 @@ def test_run_a_failing_docker_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'docker_hooks_repo', 'docker-hook-failing', - ['Hello World from docker'], b'', + ['Hello World from docker'], + mock.ANY, # an error message about `bork` not existing expected_return_code=1, ) @@ -363,6 +364,15 @@ def test_run_hook_with_curly_braced_arguments(tempdir_factory, store): ) +def test_intermixed_stdout_stderr(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'stdout_stderr_repo', + 'stdout-stderr', + [], + b'0\n1\n2\n3\n4\n5\n', + ) + + def _make_grep_repo(language, entry, store, args=()): config = { 'repo': 'local', @@ -393,20 +403,20 @@ class TestPygrep(object): def test_grep_hook_matching(self, greppable_files, store): hook = _make_grep_repo(self.language, 'ello', store) - ret, out, _ = hook.run(('f1', 'f2', 'f3')) + ret, out = hook.run(('f1', 'f2', 'f3')) assert ret == 1 assert _norm_out(out) == b"f1:1:hello'hi\n" def test_grep_hook_case_insensitive(self, greppable_files, store): hook = _make_grep_repo(self.language, 'ELLO', store, args=['-i']) - ret, out, _ = hook.run(('f1', 'f2', 'f3')) + ret, out = hook.run(('f1', 'f2', 'f3')) assert ret == 1 assert _norm_out(out) == b"f1:1:hello'hi\n" @pytest.mark.parametrize('regex', ('nope', "foo'bar", r'^\[INFO\]')) def test_grep_hook_not_matching(self, regex, greppable_files, store): hook = _make_grep_repo(self.language, regex, store) - ret, out, _ = hook.run(('f1', 'f2', 'f3')) + ret, out = hook.run(('f1', 'f2', 'f3')) assert (ret, out) == (0, b'') @@ -420,7 +430,7 @@ def test_pcre_hook_many_files(self, greppable_files, store): # file to make sure it still fails. This is not the case when naively # using a system hook with `grep -H -n '...'` hook = _make_grep_repo('pcre', 'ello', store) - ret, out, _ = hook.run((os.devnull,) * 15000 + ('f1',)) + ret, out = hook.run((os.devnull,) * 15000 + ('f1',)) assert ret == 1 assert _norm_out(out) == b"f1:1:hello'hi\n" @@ -431,7 +441,7 @@ def no_grep(exe, **kwargs): with mock.patch.object(parse_shebang, 'find_executable', no_grep): hook = _make_grep_repo('pcre', 'ello', store) - ret, out, _ = hook.run(('f1', 'f2', 'f3')) + ret, out = hook.run(('f1', 'f2', 'f3')) assert ret == 1 expected = 'Executable `{}` not found'.format(pcre.GREP).encode() assert out == expected @@ -635,7 +645,7 @@ class MyKeyboardInterrupt(KeyboardInterrupt): # However, it should be perfectly runnable (reinstall after botched # install) install_hook_envs(hooks, store) - retv, stdout, stderr = hook.run(()) + retv, stdout = hook.run(()) assert retv == 0 @@ -657,7 +667,7 @@ def test_invalidated_virtualenv(tempdir_factory, store): cmd_output_b('rm', '-rf', *paths) # pre-commit should rebuild the virtualenv and it should be runnable - retv, stdout, stderr = _get_hook(config, store, 'foo').run(()) + retv, stdout = _get_hook(config, store, 'foo').run(()) assert retv == 0 diff --git a/tests/xargs_test.py b/tests/xargs_test.py index d2d7d7b35..183ab5ad9 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -143,10 +143,9 @@ def test_argument_too_long(): def test_xargs_smoke(): - ret, out, err = xargs.xargs(('echo',), ('hello', 'world')) + ret, out = xargs.xargs(('echo',), ('hello', 'world')) assert ret == 0 assert out.replace(b'\r\n', b'\n') == b'hello world\n' - assert err == b'' exit_cmd = parse_shebang.normalize_cmd(('bash', '-c', 'exit $1', '--')) @@ -155,27 +154,27 @@ def test_xargs_smoke(): def test_xargs_negate(): - ret, _, _ = xargs.xargs( + ret, _ = xargs.xargs( exit_cmd, ('1',), negate=True, _max_length=max_length, ) assert ret == 0 - ret, _, _ = xargs.xargs( + ret, _ = xargs.xargs( exit_cmd, ('1', '0'), negate=True, _max_length=max_length, ) assert ret == 1 def test_xargs_negate_command_not_found(): - ret, _, _ = xargs.xargs(('cmd-not-found',), ('1',), negate=True) + ret, _ = xargs.xargs(('cmd-not-found',), ('1',), negate=True) assert ret != 0 def test_xargs_retcode_normal(): - ret, _, _ = xargs.xargs(exit_cmd, ('0',), _max_length=max_length) + ret, _ = xargs.xargs(exit_cmd, ('0',), _max_length=max_length) assert ret == 0 - ret, _, _ = xargs.xargs(exit_cmd, ('0', '1'), _max_length=max_length) + ret, _ = xargs.xargs(exit_cmd, ('0', '1'), _max_length=max_length) assert ret == 1 @@ -184,7 +183,7 @@ def test_xargs_concurrency(): print_pid = ('sleep 0.5 && echo $$',) start = time.time() - ret, stdout, _ = xargs.xargs( + ret, stdout = xargs.xargs( bash_cmd, print_pid * 5, target_concurrency=5, _max_length=len(' '.join(bash_cmd + print_pid)) + 1, @@ -215,6 +214,6 @@ def test_xargs_propagate_kwargs_to_cmd(): cmd = ('bash', '-c', 'echo $PRE_COMMIT_TEST_VAR', '--') cmd = parse_shebang.normalize_cmd(cmd) - ret, stdout, _ = xargs.xargs(cmd, ('1',), env=env) + ret, stdout = xargs.xargs(cmd, ('1',), env=env) assert ret == 0 assert b'Pre commit is awesome' in stdout From 38766816ac8a925102f238b2a1df11b540481526 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 12 Oct 2019 21:29:15 -0700 Subject: [PATCH 479/544] Fix fail type signature --- pre_commit/languages/fail.py | 2 +- tests/repository_test.py | 56 ++++++++++++++++++------------------ 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index f2ce09e10..164fcdbf1 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -12,4 +12,4 @@ def run_hook(hook, file_args): out = hook.entry.encode('UTF-8') + b'\n\n' out += b'\n'.join(f.encode('UTF-8') for f in file_args) + b'\n' - return 1, out, b'' + return 1, out diff --git a/tests/repository_test.py b/tests/repository_test.py index 344b3a58f..43bcd7801 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -73,9 +73,9 @@ def _test_hook_repo( ): path = make_repo(tempdir_factory, repo_path) config = make_config_from_repo(path, **(config_kwargs or {})) - ret = _get_hook(config, store, hook_id).run(args) - assert ret[0] == expected_return_code - assert _norm_out(ret[1]) == expected + ret, out = _get_hook(config, store, hook_id).run(args) + assert ret == expected_return_code + assert _norm_out(out) == expected def test_python_hook(tempdir_factory, store): @@ -137,9 +137,9 @@ def test_switch_language_versions_doesnt_clobber(tempdir_factory, store): def run_on_version(version, expected_output): config = make_config_from_repo(path) config['hooks'][0]['language_version'] = version - ret = _get_hook(config, store, 'python3-hook').run([]) - assert ret[0] == 0 - assert _norm_out(ret[1]) == expected_output + ret, out = _get_hook(config, store, 'python3-hook').run([]) + assert ret == 0 + assert _norm_out(out) == expected_output run_on_version('python2', b'2\n[]\nHello World\n') run_on_version('python3', b'3\n[]\nHello World\n') @@ -543,9 +543,9 @@ def test_local_golang_additional_dependencies(store): 'additional_dependencies': ['github.com/golang/example/hello'], }], } - ret = _get_hook(config, store, 'hello').run(()) - assert ret[0] == 0 - assert _norm_out(ret[1]) == b'Hello, Go examples!\n' + ret, out = _get_hook(config, store, 'hello').run(()) + assert ret == 0 + assert _norm_out(out) == b'Hello, Go examples!\n' def test_local_rust_additional_dependencies(store): @@ -559,9 +559,9 @@ def test_local_rust_additional_dependencies(store): 'additional_dependencies': ['cli:hello-cli:0.2.2'], }], } - ret = _get_hook(config, store, 'hello').run(()) - assert ret[0] == 0 - assert _norm_out(ret[1]) == b'Hello World!\n' + ret, out = _get_hook(config, store, 'hello').run(()) + assert ret == 0 + assert _norm_out(out) == b'Hello World!\n' def test_fail_hooks(store): @@ -576,9 +576,9 @@ def test_fail_hooks(store): }], } hook = _get_hook(config, store, 'fail') - ret = hook.run(('changelog/1234.bugfix', 'changelog/wat')) - assert ret[0] == 1 - assert ret[1] == ( + ret, out = hook.run(('changelog/1234.bugfix', 'changelog/wat')) + assert ret == 1 + assert out == ( b'make sure to name changelogs as .rst!\n' b'\n' b'changelog/1234.bugfix\n' @@ -645,8 +645,8 @@ class MyKeyboardInterrupt(KeyboardInterrupt): # However, it should be perfectly runnable (reinstall after botched # install) install_hook_envs(hooks, store) - retv, stdout = hook.run(()) - assert retv == 0 + ret, out = hook.run(()) + assert ret == 0 def test_invalidated_virtualenv(tempdir_factory, store): @@ -667,8 +667,8 @@ def test_invalidated_virtualenv(tempdir_factory, store): cmd_output_b('rm', '-rf', *paths) # pre-commit should rebuild the virtualenv and it should be runnable - retv, stdout = _get_hook(config, store, 'foo').run(()) - assert retv == 0 + ret, out = _get_hook(config, store, 'foo').run(()) + assert ret == 0 def test_really_long_file_paths(tempdir_factory, store): @@ -707,14 +707,14 @@ def test_tags_on_repositories(in_tmpdir, tempdir_factory, store): git2 = _create_repo_with_tags(tempdir_factory, 'script_hooks_repo', tag) config1 = make_config_from_repo(git1, rev=tag) - ret1 = _get_hook(config1, store, 'prints_cwd').run(('-L',)) - assert ret1[0] == 0 - assert ret1[1].strip() == _norm_pwd(in_tmpdir) + ret1, out1 = _get_hook(config1, store, 'prints_cwd').run(('-L',)) + assert ret1 == 0 + assert out1.strip() == _norm_pwd(in_tmpdir) config2 = make_config_from_repo(git2, rev=tag) - ret2 = _get_hook(config2, store, 'bash_hook').run(('bar',)) - assert ret2[0] == 0 - assert ret2[1] == b'bar\nHello World\n' + ret2, out2 = _get_hook(config2, store, 'bash_hook').run(('bar',)) + assert ret2 == 0 + assert out2 == b'bar\nHello World\n' @pytest.fixture @@ -736,9 +736,9 @@ def test_local_python_repo(store, local_python_config): hook = _get_hook(local_python_config, store, 'foo') # language_version should have been adjusted to the interpreter version assert hook.language_version != C.DEFAULT - ret = hook.run(('filename',)) - assert ret[0] == 0 - assert _norm_out(ret[1]) == b"['filename']\nHello World\n" + ret, out = hook.run(('filename',)) + assert ret == 0 + assert _norm_out(out) == b"['filename']\nHello World\n" def test_default_language_version(store, local_python_config): From 7c3404ef1f7593094c854f99bcd3b3eec75fbb2f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 12 Oct 2019 15:57:40 -0700 Subject: [PATCH 480/544] show color in hook outputs when attached to a tty --- pre_commit/commands/run.py | 9 +- pre_commit/languages/all.py | 5 +- pre_commit/languages/docker.py | 8 +- pre_commit/languages/docker_image.py | 6 +- pre_commit/languages/fail.py | 2 +- pre_commit/languages/golang.py | 4 +- pre_commit/languages/helpers.py | 10 +-- pre_commit/languages/node.py | 4 +- pre_commit/languages/pcre.py | 4 +- pre_commit/languages/pygrep.py | 4 +- pre_commit/languages/python.py | 4 +- pre_commit/languages/ruby.py | 4 +- pre_commit/languages/rust.py | 4 +- pre_commit/languages/script.py | 6 +- pre_commit/languages/swift.py | 4 +- pre_commit/languages/system.py | 4 +- pre_commit/repository.py | 9 +- pre_commit/util.py | 87 ++++++++++++++++--- pre_commit/xargs.py | 5 +- .../stdout_stderr_repo/.pre-commit-hooks.yaml | 6 +- .../{entry => stdout-stderr-entry} | 0 .../stdout_stderr_repo/tty-check-entry | 12 +++ tests/languages/all_test.py | 2 +- tests/parse_shebang_test.py | 2 +- tests/repository_test.py | 47 ++++++---- tests/util_test.py | 12 ++- tests/xargs_test.py | 12 +++ 27 files changed, 200 insertions(+), 76 deletions(-) rename testing/resources/stdout_stderr_repo/{entry => stdout-stderr-entry} (100%) create mode 100755 testing/resources/stdout_stderr_repo/tty-check-entry diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 6ab1879d0..dd30c7e59 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -73,7 +73,7 @@ def _hook_msg_start(hook, verbose): NO_FILES = '(no files to check)' -def _run_single_hook(classifier, hook, args, skips, cols): +def _run_single_hook(classifier, hook, args, skips, cols, use_color): filenames = classifier.filenames_for_hook(hook) if hook.language == 'pcre': @@ -118,7 +118,8 @@ def _run_single_hook(classifier, hook, args, skips, cols): sys.stdout.flush() diff_before = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None) - retcode, out = hook.run(tuple(filenames) if hook.pass_filenames else ()) + filenames = tuple(filenames) if hook.pass_filenames else () + retcode, out = hook.run(filenames, use_color) diff_after = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None) file_modifications = diff_before != diff_after @@ -203,7 +204,9 @@ def _run_hooks(config, hooks, args, environ): classifier = Classifier(filenames) retval = 0 for hook in hooks: - retval |= _run_single_hook(classifier, hook, args, skips, cols) + retval |= _run_single_hook( + classifier, hook, args, skips, cols, args.color, + ) if retval and config['fail_fast']: break if retval and args.show_diff_on_failure and git.has_diff(): diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 6d85ddf1f..051656b7d 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -38,16 +38,17 @@ # version - A version specified in the hook configuration or 'default'. # """ # -# def run_hook(hook, file_args): +# def run_hook(hook, file_args, color): # """Runs a hook and returns the returncode and output of running that # hook. # # Args: # hook - `Hook` # file_args - The files to be run +# color - whether the hook should be given a pty (when supported) # # Returns: -# (returncode, stdout, stderr) +# (returncode, output) # """ languages = { diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index b7a4e3223..b8cc5d07a 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -95,15 +95,15 @@ def docker_cmd(): # pragma: windows no cover ) -def run_hook(hook, file_args): # pragma: windows no cover +def run_hook(hook, file_args, color): # pragma: windows no cover assert_docker_available() # Rebuild the docker image in case it has gone missing, as many people do # automated cleanup of docker images. build_docker_image(hook.prefix, pull=False) - hook_cmd = helpers.to_cmd(hook) - entry_exe, cmd_rest = hook_cmd[0], hook_cmd[1:] + hook_cmd = hook.cmd + entry_exe, cmd_rest = hook.cmd[0], hook_cmd[1:] entry_tag = ('--entrypoint', entry_exe, docker_tag(hook.prefix)) cmd = docker_cmd() + entry_tag + cmd_rest - return helpers.run_xargs(hook, cmd, file_args) + return helpers.run_xargs(hook, cmd, file_args, color=color) diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index ab2a85654..7bd5c3140 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -12,7 +12,7 @@ install_environment = helpers.no_install -def run_hook(hook, file_args): # pragma: windows no cover +def run_hook(hook, file_args, color): # pragma: windows no cover assert_docker_available() - cmd = docker_cmd() + helpers.to_cmd(hook) - return helpers.run_xargs(hook, cmd, file_args) + cmd = docker_cmd() + hook.cmd + return helpers.run_xargs(hook, cmd, file_args, color=color) diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index 164fcdbf1..4bac1f869 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -9,7 +9,7 @@ install_environment = helpers.no_install -def run_hook(hook, file_args): +def run_hook(hook, file_args, color): out = hook.entry.encode('UTF-8') + b'\n\n' out += b'\n'.join(f.encode('UTF-8') for f in file_args) + b'\n' return 1, out diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 57984c5c5..d85a55c67 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -81,6 +81,6 @@ def install_environment(prefix, version, additional_dependencies): rmtree(pkgdir) -def run_hook(hook, file_args): +def run_hook(hook, file_args, color): with in_env(hook.prefix): - return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 8a38dec94..dab7373c0 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -3,7 +3,6 @@ import multiprocessing import os import random -import shlex import six @@ -25,10 +24,6 @@ def environment_dir(ENVIRONMENT_DIR, language_version): return '{}-{}'.format(ENVIRONMENT_DIR, language_version) -def to_cmd(hook): - return tuple(shlex.split(hook.entry)) + tuple(hook.args) - - def assert_version_default(binary, version): if version != C.DEFAULT: raise AssertionError( @@ -83,8 +78,9 @@ def _shuffled(seq): return seq -def run_xargs(hook, cmd, file_args): +def run_xargs(hook, cmd, file_args, **kwargs): # Shuffle the files so that they more evenly fill out the xargs partitions, # but do it deterministically in case a hook cares about ordering. file_args = _shuffled(file_args) - return xargs(cmd, file_args, target_concurrency=target_concurrency(hook)) + kwargs['target_concurrency'] = target_concurrency(hook) + return xargs(cmd, file_args, **kwargs) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 1cb947a04..f5bc9bfaa 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -78,6 +78,6 @@ def install_environment( ) -def run_hook(hook, file_args): # pragma: windows no cover +def run_hook(hook, file_args, color): # pragma: windows no cover with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py index 143adb231..2d8bdfa01 100644 --- a/pre_commit/languages/pcre.py +++ b/pre_commit/languages/pcre.py @@ -13,10 +13,10 @@ install_environment = helpers.no_install -def run_hook(hook, file_args): +def run_hook(hook, file_args, color): # For PCRE the entry is the regular expression to match cmd = (GREP, '-H', '-n', '-P') + tuple(hook.args) + (hook.entry,) # Grep usually returns 0 for matches, and nonzero for non-matches so we # negate it here. - return xargs(cmd, file_args, negate=True) + return xargs(cmd, file_args, negate=True, color=color) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index e0188a974..ae1fa90ec 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -44,9 +44,9 @@ def _process_filename_at_once(pattern, filename): return retv -def run_hook(hook, file_args): +def run_hook(hook, file_args, color): exe = (sys.executable, '-m', __name__) + tuple(hook.args) + (hook.entry,) - return xargs(exe, file_args) + return xargs(exe, file_args, color=color) def main(argv=None): diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 948b28973..c9bedb68c 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -151,9 +151,9 @@ def healthy(prefix, language_version): ) return retcode == 0 - def run_hook(hook, file_args): + def run_hook(hook, file_args, color): with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) def install_environment(prefix, version, additional_dependencies): additional_dependencies = tuple(additional_dependencies) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index c721b3ceb..83e2a6faf 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -124,6 +124,6 @@ def install_environment( ) -def run_hook(hook, file_args): # pragma: windows no cover +def run_hook(hook, file_args, color): # pragma: windows no cover with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 9885c3c4d..91291fb34 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -89,6 +89,6 @@ def install_environment(prefix, version, additional_dependencies): ) -def run_hook(hook, file_args): +def run_hook(hook, file_args, color): with in_env(hook.prefix): - return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 56d9d27e9..96b8aeb6f 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -9,7 +9,7 @@ install_environment = helpers.no_install -def run_hook(hook, file_args): - cmd = helpers.to_cmd(hook) +def run_hook(hook, file_args, color): + cmd = hook.cmd cmd = (hook.prefix.path(cmd[0]),) + cmd[1:] - return helpers.run_xargs(hook, cmd, file_args) + return helpers.run_xargs(hook, cmd, file_args, color=color) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 9e1bf62f7..014349596 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -51,6 +51,6 @@ def install_environment( ) -def run_hook(hook, file_args): # pragma: windows no cover +def run_hook(hook, file_args, color): # pragma: windows no cover with in_env(hook.prefix): - return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 5a22670e0..b412b368c 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -9,5 +9,5 @@ install_environment = helpers.no_install -def run_hook(hook, file_args): - return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) +def run_hook(hook, file_args, color): + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 5b12a98c8..3042f12dc 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -5,6 +5,7 @@ import json import logging import os +import shlex import pre_commit.constants as C from pre_commit import five @@ -54,6 +55,10 @@ def _write_state(prefix, venv, state): class Hook(collections.namedtuple('Hook', ('src', 'prefix') + _KEYS)): __slots__ = () + @property + def cmd(self): + return tuple(shlex.split(self.entry)) + tuple(self.args) + @property def install_key(self): return ( @@ -95,9 +100,9 @@ def install(self): # Write our state to indicate we're installed _write_state(self.prefix, venv, _state(self.additional_dependencies)) - def run(self, file_args): + def run(self, file_args, color): lang = languages[self.language] - return lang.run_hook(self, file_args) + return lang.run_hook(self, file_args, color) @classmethod def create(cls, src, prefix, dct): diff --git a/pre_commit/util.py b/pre_commit/util.py index 1a93a2333..0f54e9e1e 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -117,29 +117,28 @@ def to_text(self): __str__ = to_text -def cmd_output_b(*cmd, **kwargs): - retcode = kwargs.pop('retcode', 0) - - popen_kwargs = { - 'stdin': subprocess.PIPE, - 'stdout': subprocess.PIPE, - 'stderr': subprocess.PIPE, - } - +def _cmd_kwargs(*cmd, **kwargs): # py2/py3 on windows are more strict about the types here cmd = tuple(five.n(arg) for arg in cmd) kwargs['env'] = { five.n(key): five.n(value) for key, value in kwargs.pop('env', {}).items() } or None - popen_kwargs.update(kwargs) + for arg in ('stdin', 'stdout', 'stderr'): + kwargs.setdefault(arg, subprocess.PIPE) + return cmd, kwargs + + +def cmd_output_b(*cmd, **kwargs): + retcode = kwargs.pop('retcode', 0) + cmd, kwargs = _cmd_kwargs(*cmd, **kwargs) try: cmd = parse_shebang.normalize_cmd(cmd) except parse_shebang.ExecutableNotFoundError as e: returncode, stdout_b, stderr_b = e.to_output() else: - proc = subprocess.Popen(cmd, **popen_kwargs) + proc = subprocess.Popen(cmd, **kwargs) stdout_b, stderr_b = proc.communicate() returncode = proc.returncode @@ -158,6 +157,72 @@ def cmd_output(*cmd, **kwargs): return returncode, stdout, stderr +if os.name != 'nt': # pragma: windows no cover + from os import openpty + import termios + + class Pty(object): + def __init__(self): + self.r = self.w = None + + def __enter__(self): + self.r, self.w = openpty() + + # tty flags normally change \n to \r\n + attrs = termios.tcgetattr(self.r) + attrs[1] &= ~(termios.ONLCR | termios.OPOST) + termios.tcsetattr(self.r, termios.TCSANOW, attrs) + + return self + + def close_w(self): + if self.w is not None: + os.close(self.w) + self.w = None + + def close_r(self): + assert self.r is not None + os.close(self.r) + self.r = None + + def __exit__(self, exc_type, exc_value, traceback): + self.close_w() + self.close_r() + + def cmd_output_p(*cmd, **kwargs): + assert kwargs.pop('retcode') is None + assert kwargs['stderr'] == subprocess.STDOUT, kwargs['stderr'] + cmd, kwargs = _cmd_kwargs(*cmd, **kwargs) + + try: + cmd = parse_shebang.normalize_cmd(cmd) + except parse_shebang.ExecutableNotFoundError as e: + return e.to_output() + + with open(os.devnull) as devnull, Pty() as pty: + kwargs.update({'stdin': devnull, 'stdout': pty.w, 'stderr': pty.w}) + proc = subprocess.Popen(cmd, **kwargs) + pty.close_w() + + buf = b'' + while True: + try: + bts = os.read(pty.r, 4096) + except OSError as e: + if e.errno == errno.EIO: + bts = b'' + else: + raise + else: + buf += bts + if not bts: + break + + return proc.wait(), buf, None +else: # pragma: no cover + cmd_output_p = cmd_output_b + + def rmtree(path): """On windows, rmtree fails for readonly dirs.""" def handle_remove_readonly(func, path, exc): diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 440317545..4c3ddacfc 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -13,6 +13,7 @@ from pre_commit import parse_shebang from pre_commit.util import cmd_output_b +from pre_commit.util import cmd_output_p def _environ_size(_env=None): @@ -108,9 +109,11 @@ def xargs(cmd, varargs, **kwargs): negate: Make nonzero successful and zero a failure target_concurrency: Target number of partitions to run concurrently """ + color = kwargs.pop('color', False) negate = kwargs.pop('negate', False) target_concurrency = kwargs.pop('target_concurrency', 1) max_length = kwargs.pop('_max_length', _get_platform_max_length()) + cmd_fn = cmd_output_p if color else cmd_output_b retcode = 0 stdout = b'' @@ -122,7 +125,7 @@ def xargs(cmd, varargs, **kwargs): partitions = partition(cmd, varargs, target_concurrency, max_length) def run_cmd_partition(run_cmd): - return cmd_output_b( + return cmd_fn( *run_cmd, retcode=None, stderr=subprocess.STDOUT, **kwargs ) diff --git a/testing/resources/stdout_stderr_repo/.pre-commit-hooks.yaml b/testing/resources/stdout_stderr_repo/.pre-commit-hooks.yaml index e68174a12..6800d2593 100644 --- a/testing/resources/stdout_stderr_repo/.pre-commit-hooks.yaml +++ b/testing/resources/stdout_stderr_repo/.pre-commit-hooks.yaml @@ -1,4 +1,8 @@ - id: stdout-stderr name: stdout-stderr language: script - entry: ./entry + entry: ./stdout-stderr-entry +- id: tty-check + name: tty-check + language: script + entry: ./tty-check-entry diff --git a/testing/resources/stdout_stderr_repo/entry b/testing/resources/stdout_stderr_repo/stdout-stderr-entry similarity index 100% rename from testing/resources/stdout_stderr_repo/entry rename to testing/resources/stdout_stderr_repo/stdout-stderr-entry diff --git a/testing/resources/stdout_stderr_repo/tty-check-entry b/testing/resources/stdout_stderr_repo/tty-check-entry new file mode 100755 index 000000000..8c6530ec8 --- /dev/null +++ b/testing/resources/stdout_stderr_repo/tty-check-entry @@ -0,0 +1,12 @@ +#!/usr/bin/env python +import sys + + +def main(): + print('stdin: {}'.format(sys.stdin.isatty())) + print('stdout: {}'.format(sys.stdout.isatty())) + print('stderr: {}'.format(sys.stderr.isatty())) + + +if __name__ == '__main__': + exit(main()) diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index 967544198..2185ae0d2 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -39,7 +39,7 @@ def test_ENVIRONMENT_DIR(language): @pytest.mark.parametrize('language', all_languages) def test_run_hook_argpsec(language): - expected_argspec = ArgSpec(args=['hook', 'file_args']) + expected_argspec = ArgSpec(args=['hook', 'file_args', 'color']) argspec = getargspec(languages[language].run_hook) assert argspec == expected_argspec diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index 589533226..fe1cdcd1f 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -91,7 +91,7 @@ def test_normexe_does_not_exist_sep(): assert excinfo.value.args == ('Executable `./i-dont-exist-lol` not found',) -@pytest.mark.xfail(os.name == 'nt', reason='posix only',) +@pytest.mark.xfail(os.name == 'nt', reason='posix only') def test_normexe_not_executable(tmpdir): # pragma: windows no cover tmpdir.join('exe').ensure() with tmpdir.as_cwd(), pytest.raises(OSError) as excinfo: diff --git a/tests/repository_test.py b/tests/repository_test.py index 43bcd7801..85afa90d3 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -70,10 +70,11 @@ def _test_hook_repo( expected, expected_return_code=0, config_kwargs=None, + color=False, ): path = make_repo(tempdir_factory, repo_path) config = make_config_from_repo(path, **(config_kwargs or {})) - ret, out = _get_hook(config, store, hook_id).run(args) + ret, out = _get_hook(config, store, hook_id).run(args, color=color) assert ret == expected_return_code assert _norm_out(out) == expected @@ -137,7 +138,8 @@ def test_switch_language_versions_doesnt_clobber(tempdir_factory, store): def run_on_version(version, expected_output): config = make_config_from_repo(path) config['hooks'][0]['language_version'] = version - ret, out = _get_hook(config, store, 'python3-hook').run([]) + hook = _get_hook(config, store, 'python3-hook') + ret, out = hook.run([], color=False) assert ret == 0 assert _norm_out(out) == expected_output @@ -373,6 +375,17 @@ def test_intermixed_stdout_stderr(tempdir_factory, store): ) +@pytest.mark.xfail(os.name == 'nt', reason='ptys are posix-only') +def test_output_isatty(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'stdout_stderr_repo', + 'tty-check', + [], + b'stdin: False\nstdout: True\nstderr: True\n', + color=True, + ) + + def _make_grep_repo(language, entry, store, args=()): config = { 'repo': 'local', @@ -403,20 +416,20 @@ class TestPygrep(object): def test_grep_hook_matching(self, greppable_files, store): hook = _make_grep_repo(self.language, 'ello', store) - ret, out = hook.run(('f1', 'f2', 'f3')) + ret, out = hook.run(('f1', 'f2', 'f3'), color=False) assert ret == 1 assert _norm_out(out) == b"f1:1:hello'hi\n" def test_grep_hook_case_insensitive(self, greppable_files, store): hook = _make_grep_repo(self.language, 'ELLO', store, args=['-i']) - ret, out = hook.run(('f1', 'f2', 'f3')) + ret, out = hook.run(('f1', 'f2', 'f3'), color=False) assert ret == 1 assert _norm_out(out) == b"f1:1:hello'hi\n" @pytest.mark.parametrize('regex', ('nope', "foo'bar", r'^\[INFO\]')) def test_grep_hook_not_matching(self, regex, greppable_files, store): hook = _make_grep_repo(self.language, regex, store) - ret, out = hook.run(('f1', 'f2', 'f3')) + ret, out = hook.run(('f1', 'f2', 'f3'), color=False) assert (ret, out) == (0, b'') @@ -430,7 +443,7 @@ def test_pcre_hook_many_files(self, greppable_files, store): # file to make sure it still fails. This is not the case when naively # using a system hook with `grep -H -n '...'` hook = _make_grep_repo('pcre', 'ello', store) - ret, out = hook.run((os.devnull,) * 15000 + ('f1',)) + ret, out = hook.run((os.devnull,) * 15000 + ('f1',), color=False) assert ret == 1 assert _norm_out(out) == b"f1:1:hello'hi\n" @@ -441,7 +454,7 @@ def no_grep(exe, **kwargs): with mock.patch.object(parse_shebang, 'find_executable', no_grep): hook = _make_grep_repo('pcre', 'ello', store) - ret, out = hook.run(('f1', 'f2', 'f3')) + ret, out = hook.run(('f1', 'f2', 'f3'), color=False) assert ret == 1 expected = 'Executable `{}` not found'.format(pcre.GREP).encode() assert out == expected @@ -543,7 +556,7 @@ def test_local_golang_additional_dependencies(store): 'additional_dependencies': ['github.com/golang/example/hello'], }], } - ret, out = _get_hook(config, store, 'hello').run(()) + ret, out = _get_hook(config, store, 'hello').run((), color=False) assert ret == 0 assert _norm_out(out) == b'Hello, Go examples!\n' @@ -559,7 +572,7 @@ def test_local_rust_additional_dependencies(store): 'additional_dependencies': ['cli:hello-cli:0.2.2'], }], } - ret, out = _get_hook(config, store, 'hello').run(()) + ret, out = _get_hook(config, store, 'hello').run((), color=False) assert ret == 0 assert _norm_out(out) == b'Hello World!\n' @@ -576,12 +589,12 @@ def test_fail_hooks(store): }], } hook = _get_hook(config, store, 'fail') - ret, out = hook.run(('changelog/1234.bugfix', 'changelog/wat')) + ret, out = hook.run(('changelog/123.bugfix', 'changelog/wat'), color=False) assert ret == 1 assert out == ( b'make sure to name changelogs as .rst!\n' b'\n' - b'changelog/1234.bugfix\n' + b'changelog/123.bugfix\n' b'changelog/wat\n' ) @@ -645,7 +658,7 @@ class MyKeyboardInterrupt(KeyboardInterrupt): # However, it should be perfectly runnable (reinstall after botched # install) install_hook_envs(hooks, store) - ret, out = hook.run(()) + ret, out = hook.run((), color=False) assert ret == 0 @@ -667,7 +680,7 @@ def test_invalidated_virtualenv(tempdir_factory, store): cmd_output_b('rm', '-rf', *paths) # pre-commit should rebuild the virtualenv and it should be runnable - ret, out = _get_hook(config, store, 'foo').run(()) + ret, out = _get_hook(config, store, 'foo').run((), color=False) assert ret == 0 @@ -707,12 +720,14 @@ def test_tags_on_repositories(in_tmpdir, tempdir_factory, store): git2 = _create_repo_with_tags(tempdir_factory, 'script_hooks_repo', tag) config1 = make_config_from_repo(git1, rev=tag) - ret1, out1 = _get_hook(config1, store, 'prints_cwd').run(('-L',)) + hook1 = _get_hook(config1, store, 'prints_cwd') + ret1, out1 = hook1.run(('-L',), color=False) assert ret1 == 0 assert out1.strip() == _norm_pwd(in_tmpdir) config2 = make_config_from_repo(git2, rev=tag) - ret2, out2 = _get_hook(config2, store, 'bash_hook').run(('bar',)) + hook2 = _get_hook(config2, store, 'bash_hook') + ret2, out2 = hook2.run(('bar',), color=False) assert ret2 == 0 assert out2 == b'bar\nHello World\n' @@ -736,7 +751,7 @@ def test_local_python_repo(store, local_python_config): hook = _get_hook(local_python_config, store, 'foo') # language_version should have been adjusted to the interpreter version assert hook.language_version != C.DEFAULT - ret, out = hook.run(('filename',)) + ret, out = hook.run(('filename',), color=False) assert ret == 0 assert _norm_out(out) == b"['filename']\nHello World\n" diff --git a/tests/util_test.py b/tests/util_test.py index 867969c34..dd1ad37bd 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -2,12 +2,14 @@ import os.path import stat +import subprocess import pytest from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_p from pre_commit.util import parse_version from pre_commit.util import rmtree from pre_commit.util import tmpdir @@ -83,9 +85,15 @@ def test_tmpdir(): def test_cmd_output_exe_not_found(): - ret, out, _ = cmd_output('i-dont-exist', retcode=None) + ret, out, _ = cmd_output('dne', retcode=None) assert ret == 1 - assert out == 'Executable `i-dont-exist` not found' + assert out == 'Executable `dne` not found' + + +def test_cmd_output_p_exe_not_found(): + ret, out, _ = cmd_output_p('dne', retcode=None, stderr=subprocess.STDOUT) + assert ret == 1 + assert out == b'Executable `dne` not found' def test_parse_version(): diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 183ab5ad9..a6772804a 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import concurrent.futures +import os import sys import time @@ -217,3 +218,14 @@ def test_xargs_propagate_kwargs_to_cmd(): ret, stdout = xargs.xargs(cmd, ('1',), env=env) assert ret == 0 assert b'Pre commit is awesome' in stdout + + +@pytest.mark.xfail(os.name == 'nt', reason='posix only') +def test_xargs_color_true_makes_tty(): + retcode, out = xargs.xargs( + (sys.executable, '-c', 'import sys; print(sys.stdout.isatty())'), + ('1',), + color=True, + ) + assert retcode == 0 + assert out == b'True\n' From f8f81db36d3f43cee3a73e3377ce07df93b54d0e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 17 Oct 2019 10:48:35 -0700 Subject: [PATCH 481/544] Use importlib.metadata directly in python3.8+ --- azure-pipelines.yml | 2 +- pre_commit/constants.py | 7 ++++++- setup.cfg | 10 +++++----- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 30b873a0c..5b57e8948 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -36,7 +36,7 @@ jobs: displayName: install swift - template: job--python-tox.yml@asottile parameters: - toxenvs: [pypy, pypy3, py27, py36, py37] + toxenvs: [pypy, pypy3, py27, py36, py37, py38] os: linux pre_test: - task: UseRubyVersion@0 diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 307b09a45..7dd447c0f 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -1,7 +1,12 @@ from __future__ import absolute_import from __future__ import unicode_literals -import importlib_metadata # TODO: importlib.metadata py38? +import sys + +if sys.version_info < (3, 8): # pragma: no cover (=2.0.0 identify>=1.0.0 - importlib-metadata nodeenv>=0.11.1 pyyaml six toml virtualenv>=15.2 - futures; python_version<"3.2" - importlib-resources; python_version<"3.7" -python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* + futures;python_version<"3.2" + importlib-metadata;python_version<"3.8" + importlib-resources;python_version<"3.7" +python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* [options.entry_points] console_scripts = From 707407dd49bf556dfaaf7553fe0c16f8408d1acc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 19 Oct 2019 12:29:46 -0700 Subject: [PATCH 482/544] Normalize paths on windows to forward slashes --- pre_commit/commands/run.py | 6 ++++++ tests/commands/run_test.py | 20 +++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index dd30c7e59..0b1f7b7ea 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -34,6 +34,12 @@ def filter_by_include_exclude(names, include, exclude): class Classifier(object): def __init__(self, filenames): + # on windows we normalize all filenames to use forward slashes + # this makes it easier to filter using the `files:` regex + # this also makes improperly quoted shell-based hooks work better + # see #1173 + if os.altsep == '/' and os.sep == '\\': + filenames = (f.replace(os.sep, os.altsep) for f in filenames) self.filenames = [f for f in filenames if os.path.lexists(f)] self._types_cache = {} diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index f6d5c93f5..4221134bc 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -7,6 +7,7 @@ import subprocess import sys +import mock import pytest import pre_commit.constants as C @@ -782,7 +783,7 @@ def test_files_running_subdir(repo_with_passing_hook, tempdir_factory): '--files', 'foo.py', tempdir_factory=tempdir_factory, ) - assert 'subdir/foo.py'.replace('/', os.sep) in stdout + assert 'subdir/foo.py' in stdout @pytest.mark.parametrize( @@ -826,6 +827,23 @@ def test_classifier_removes_dne(): assert classifier.filenames == [] +def test_classifier_normalizes_filenames_on_windows_to_forward_slashes(tmpdir): + with tmpdir.as_cwd(): + tmpdir.join('a/b/c').ensure() + with mock.patch.object(os, 'altsep', '/'): + with mock.patch.object(os, 'sep', '\\'): + classifier = Classifier((r'a\b\c',)) + assert classifier.filenames == ['a/b/c'] + + +def test_classifier_does_not_normalize_backslashes_non_windows(tmpdir): + with mock.patch.object(os.path, 'lexists', return_value=True): + with mock.patch.object(os, 'altsep', None): + with mock.patch.object(os, 'sep', '/'): + classifier = Classifier((r'a/b\c',)) + assert classifier.filenames == [r'a/b\c'] + + @pytest.fixture def some_filenames(): return ( From bfcee8ec9fb5ab8390a225ba6fa64607d50eacb9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 22 Oct 2019 13:23:57 -0700 Subject: [PATCH 483/544] Fix python.healthy() check with stdlib module clashes --- pre_commit/languages/python.py | 1 + tests/languages/python_test.py | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index c9bedb68c..6eecc0c83 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -147,6 +147,7 @@ def healthy(prefix, language_version): retcode, _, _ = cmd_output_b( 'python', '-c', 'import ctypes, datetime, io, os, ssl, weakref', + cwd='/', retcode=None, ) return retcode == 0 diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index d9d8ecd5b..7daff1d41 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -7,7 +7,9 @@ import mock import pytest +import pre_commit.constants as C from pre_commit.languages import python +from pre_commit.prefix import Prefix def test_norm_version_expanduser(): @@ -48,3 +50,11 @@ def test_find_by_sys_executable(exe, realpath, expected): with mock.patch.object(os.path, 'realpath', return_value=realpath): with mock.patch.object(python, 'find_executable', lambda x: x): assert python._find_by_sys_executable() == expected + + +def test_healthy_types_py_in_cwd(tmpdir): + with tmpdir.as_cwd(): + # even if a `types.py` file exists, should still be healthy + tmpdir.join('types.py').ensure() + # this env doesn't actually exist (for test speed purposes) + assert python.healthy(Prefix(str(tmpdir)), C.DEFAULT) is True From f1b6a7842a0b184410746ce506c5a78f955a3075 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 26 Oct 2019 12:45:55 -0700 Subject: [PATCH 484/544] v1.19.0 --- CHANGELOG.md | 38 ++++++++++++++++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f7811cbe..7012a93a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,41 @@ +1.19.0 - 2019-10-26 +=================== + +### Features +- Allow `--hook-type` to be specified multiple times. + - example: `pre-commit install --hook-type pre-commit --hook-type pre-push` + - #1139 issue by @MaxymVlasov. + - #1145 PR by @asottile. +- Include more version information in crash logs. + - #1142 by @marqueewinq. +- Hook colors are now passed through on platforms which support `pty`. + - #1169 by @asottile. +- pre-commit now uses `importlib.metadata` directly when running in python 3.8 + - #1176 by @asottile. +- Normalize paths to forward slash separators on windows. + - makes it easier to match paths with `files:` regex + - avoids some quoting bugs in shell-based hooks + - #1173 issue by @steigenTI. + - #1179 PR by @asottile. + +### Fixes +- Remove some extra newlines from error messages. + - #1148 by @asottile. +- When a hook is not executable it now reports `not executable` instead of + `not found`. + - #1159 issue by @nixjdm. + - #1161 PR by @WillKoehrsen. +- Fix interleaving of stdout / stderr in hooks. + - #1168 by @asottile. +- Fix python environment `healthy()` check when current working directory + contains modules which shadow standard library names. + - issue by @vwhsu92. + - #1185 PR by @asottile. + +### Updating +- Regexes handling both backslashes and forward slashes for directory + separators now only need to handle forward slashes. + 1.18.3 - 2019-08-27 =================== diff --git a/setup.cfg b/setup.cfg index 1ac2608b4..cf8e34202 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.18.3 +version = 1.19.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 1bd745eccf2d9ea192bcdc7754fabdea5d766e64 Mon Sep 17 00:00:00 2001 From: John Cooper Date: Mon, 28 Oct 2019 19:48:13 +0000 Subject: [PATCH 485/544] Added new versions of rbenv and ruby-build --- pre_commit/make_archives.py | 4 ++-- pre_commit/resources/rbenv.tar.gz | Bin 31433 -> 31781 bytes pre_commit/resources/ruby-build.tar.gz | Bin 52443 -> 62567 bytes 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index cff45d0cd..1542548dc 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -17,8 +17,8 @@ REPOS = ( - ('rbenv', 'git://github.com/rbenv/rbenv', 'e60ad4a'), - ('ruby-build', 'git://github.com/rbenv/ruby-build', '9bc9971'), + ('rbenv', 'git://github.com/rbenv/rbenv', 'a3fa9b7'), + ('ruby-build', 'git://github.com/rbenv/ruby-build', '1a902f3'), ( 'ruby-download', 'git://github.com/garnieretienne/rvm-download', diff --git a/pre_commit/resources/rbenv.tar.gz b/pre_commit/resources/rbenv.tar.gz index 4505e47142d62f9d03002f885ec91e1a9beef90e..5307b19d63ef650032717a5ebbf667198809dfee 100644 GIT binary patch literal 31781 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�yL@%#;VS(jc*bu^U#nB?@eMuF2voBlixC} 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 diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz index 66107ddf2da23de847c9719647882631f1f23ba2..4a69a0991649283220d7370d7804fe792cadacfa 100644 GIT binary patch literal 62567 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 BOpgEn literal 52443 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 A+5i9m From 8ff880faa63084afd0abdae7d00f902f31cb3db6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 28 Oct 2019 14:56:03 -0700 Subject: [PATCH 486/544] Fix rare first-run race with creation of pre-commit directory --- pre_commit/store.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index 5215d80a7..2f1592444 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -13,6 +13,7 @@ from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b +from pre_commit.util import mkdirp from pre_commit.util import resource_text from pre_commit.util import rmtree @@ -41,7 +42,7 @@ def __init__(self, directory=None): self.db_path = os.path.join(self.directory, 'db.db') if not os.path.exists(self.directory): - os.makedirs(self.directory) + mkdirp(self.directory) with io.open(os.path.join(self.directory, 'README'), 'w') as f: f.write( 'This directory is maintained by the pre-commit project.\n' From 54359fff395c6890fcc4939a4ea650fede8c8197 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 28 Oct 2019 15:21:28 -0700 Subject: [PATCH 487/544] Bump the version of pre-commit-hooks in sample-config --- pre_commit/commands/sample_config.py | 2 +- tests/commands/sample_config_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/sample_config.py b/pre_commit/commands/sample_config.py index 38320f67b..a35ef8e5c 100644 --- a/pre_commit/commands/sample_config.py +++ b/pre_commit/commands/sample_config.py @@ -12,7 +12,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.0.0 + rev: v2.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/tests/commands/sample_config_test.py b/tests/commands/sample_config_test.py index 83942a4f0..57ef3a494 100644 --- a/tests/commands/sample_config_test.py +++ b/tests/commands/sample_config_test.py @@ -13,7 +13,7 @@ def test_sample_config(capsys): # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.0.0 + rev: v2.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer From 0bc40bc4ea081802dd41ac92068104dfde468f6d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 28 Oct 2019 16:29:59 -0700 Subject: [PATCH 488/544] v1.20.0 --- CHANGELOG.md | 15 +++++++++++++++ setup.cfg | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7012a93a6..289056654 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +1.20.0 - 2019-10-28 +=================== + +### Features +- Allow building newer versions of `ruby`. + - #1193 issue by @choffee. + - #1195 PR by @choffee. +- Bump versions reported in `pre-commit sample-config`. + - #1197 PR by @asottile. + +### Fixes +- Fix rare race condition with multiple concurrent first-time runs. + - #1192 issue by @raholler. + - #1196 PR by @asottile. + 1.19.0 - 2019-10-26 =================== diff --git a/setup.cfg b/setup.cfg index cf8e34202..f9ae6e377 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.19.0 +version = 1.20.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 8dd05c9fce5204a6237f58b06389b264e22f6a4c Mon Sep 17 00:00:00 2001 From: Ryan Rhee Date: Fri, 1 Nov 2019 09:15:38 -0400 Subject: [PATCH 489/544] [xargs] Update docblock --- pre_commit/xargs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 4c3ddacfc..f48f31364 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -106,6 +106,7 @@ def _thread_mapper(maxsize): def xargs(cmd, varargs, **kwargs): """A simplified implementation of xargs. + color: Make a pty if on a platform that supports it negate: Make nonzero successful and zero a failure target_concurrency: Target number of partitions to run concurrently """ From addc7045bacbd7addb7b6627ba6d98d7cc289e43 Mon Sep 17 00:00:00 2001 From: Ryan Rhee Date: Fri, 1 Nov 2019 11:33:04 -0400 Subject: [PATCH 490/544] grammar --- pre_commit/languages/docker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index b8cc5d07a..66f5a7c98 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -69,7 +69,7 @@ def install_environment( ) # Docker doesn't really have relevant disk environment, but pre-commit - # still needs to cleanup it's state files on failure + # still needs to cleanup its state files on failure with clean_path_on_failure(directory): build_docker_image(prefix, pull=True) os.mkdir(directory) From 0760bec3ffe1cbabdbead1909aac38aae14c7732 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 18 Nov 2019 14:57:41 -0800 Subject: [PATCH 491/544] Show better error message when running inside `.git` --- pre_commit/main.py | 10 +++++++++- tests/main_test.py | 5 +++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 59de5f24c..772c69cb3 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -122,12 +122,20 @@ def _adjust_args_and_chdir(args): args.repo = os.path.abspath(args.repo) try: - os.chdir(git.get_root()) + toplevel = git.get_root() except CalledProcessError: raise FatalError( 'git failed. Is it installed, and are you in a Git repository ' 'directory?', ) + else: + if toplevel == '': + raise FatalError( + 'git toplevel unexpectedly empty! make sure you are not ' + 'inside the `.git` directory of your repository.', + ) + else: + os.chdir(toplevel) args.config = os.path.relpath(args.config) if args.command in {'run', 'try-repo'}: diff --git a/tests/main_test.py b/tests/main_test.py index 364e0d390..b59d35ef1 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -39,6 +39,11 @@ def test_adjust_args_and_chdir_not_in_git_dir(in_tmpdir): main._adjust_args_and_chdir(Args()) +def test_adjust_args_and_chdir_in_dot_git_dir(in_git_dir): + with in_git_dir.join('.git').as_cwd(), pytest.raises(FatalError): + main._adjust_args_and_chdir(Args()) + + def test_adjust_args_and_chdir_noop(in_git_dir): args = Args(command='run', files=['f1', 'f2']) main._adjust_args_and_chdir(args) From dc612f0219a6c919b4f5a9be18c43b4b3ddce99a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 6 Dec 2019 09:08:26 -0800 Subject: [PATCH 492/544] Fix step template breakage --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 5b57e8948..e797b0c8e 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -29,7 +29,7 @@ jobs: name_postfix: _latest_git pre_test: - task: UseRubyVersion@0 - - template: step--git-install.yml + - template: step--git-install.yml@asottile - bash: | testing/get-swift.sh echo '##vso[task.prependpath]/tmp/swift/usr/bin' From 2bdbd9e7a0f1556921946bc441b595d6689fbcd0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 6 Dec 2019 09:27:19 -0800 Subject: [PATCH 493/544] Fix for newest git --- pre_commit/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 772c69cb3..686ddc4c2 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -129,7 +129,7 @@ def _adjust_args_and_chdir(args): 'directory?', ) else: - if toplevel == '': + if toplevel == '': # pragma: no cover (old git) raise FatalError( 'git toplevel unexpectedly empty! make sure you are not ' 'inside the `.git` directory of your repository.', From 9fada617b981be4f9e5ea62de920b380d1b35c02 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 6 Dec 2019 11:27:36 -0800 Subject: [PATCH 494/544] Use echo instead of python in parse_shebang_test --- tests/parse_shebang_test.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index fe1cdcd1f..84ace31c9 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -21,9 +21,9 @@ def test_file_doesnt_exist(): def test_simple_case(tmpdir): x = tmpdir.join('f') - x.write_text('#!/usr/bin/env python', encoding='UTF-8') + x.write_text('#!/usr/bin/env echo', encoding='UTF-8') make_executable(x.strpath) - assert parse_shebang.parse_filename(x.strpath) == ('python',) + assert parse_shebang.parse_filename(x.strpath) == ('echo',) def test_find_executable_full_path(): @@ -125,28 +125,28 @@ def test_normalize_cmd_trivial(): def test_normalize_cmd_PATH(): - cmd = ('python', '--version') - expected = (distutils.spawn.find_executable('python'), '--version') + cmd = ('echo', '--version') + expected = (distutils.spawn.find_executable('echo'), '--version') assert parse_shebang.normalize_cmd(cmd) == expected def test_normalize_cmd_shebang(in_tmpdir): - python = distutils.spawn.find_executable('python').replace(os.sep, '/') - path = write_executable(python) - assert parse_shebang.normalize_cmd((path,)) == (python, path) + echo = distutils.spawn.find_executable('echo').replace(os.sep, '/') + path = write_executable(echo) + assert parse_shebang.normalize_cmd((path,)) == (echo, path) def test_normalize_cmd_PATH_shebang_full_path(in_tmpdir): - python = distutils.spawn.find_executable('python').replace(os.sep, '/') - path = write_executable(python) + echo = distutils.spawn.find_executable('echo').replace(os.sep, '/') + path = write_executable(echo) with bin_on_path(): ret = parse_shebang.normalize_cmd(('run',)) - assert ret == (python, os.path.abspath(path)) + assert ret == (echo, os.path.abspath(path)) def test_normalize_cmd_PATH_shebang_PATH(in_tmpdir): - python = distutils.spawn.find_executable('python') - path = write_executable('/usr/bin/env python') + echo = distutils.spawn.find_executable('echo') + path = write_executable('/usr/bin/env echo') with bin_on_path(): ret = parse_shebang.normalize_cmd(('run',)) - assert ret == (python, os.path.abspath(path)) + assert ret == (echo, os.path.abspath(path)) From f6b0c135ce2d925666dda99bce4e1f744d3c3b51 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 6 Dec 2019 13:26:50 -0800 Subject: [PATCH 495/544] Create an actual environment for python healthy() types test --- tests/languages/python_test.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 7daff1d41..55854a8a7 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -54,7 +54,11 @@ def test_find_by_sys_executable(exe, realpath, expected): def test_healthy_types_py_in_cwd(tmpdir): with tmpdir.as_cwd(): + prefix = tmpdir.join('prefix').ensure_dir() + prefix.join('setup.py').write('import setuptools; setuptools.setup()') + prefix = Prefix(str(prefix)) + python.install_environment(prefix, C.DEFAULT, ()) + # even if a `types.py` file exists, should still be healthy tmpdir.join('types.py').ensure() - # this env doesn't actually exist (for test speed purposes) - assert python.healthy(Prefix(str(tmpdir)), C.DEFAULT) is True + assert python.healthy(prefix, C.DEFAULT) is True From 2cff185c00540cb7a5d305db5e1726a4aad0cd1a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 6 Dec 2019 13:35:28 -0800 Subject: [PATCH 496/544] Revert "Fix step template breakage" This reverts commit dc612f0219a6c919b4f5a9be18c43b4b3ddce99a. --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index e797b0c8e..5b57e8948 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -29,7 +29,7 @@ jobs: name_postfix: _latest_git pre_test: - task: UseRubyVersion@0 - - template: step--git-install.yml@asottile + - template: step--git-install.yml - bash: | testing/get-swift.sh echo '##vso[task.prependpath]/tmp/swift/usr/bin' From 4ff23b4eab946d5fb31f07aeacddc5d84f88369f Mon Sep 17 00:00:00 2001 From: "Uwe L. Korn" Date: Mon, 2 Dec 2019 15:18:54 +0100 Subject: [PATCH 497/544] Support for conda as a language --- azure-pipelines.yml | 3 + pre_commit/languages/all.py | 2 + pre_commit/languages/conda.py | 66 +++++++++++++++++++ .../resources/empty_template_environment.yml | 9 +++ pre_commit/store.py | 2 +- .../conda_hooks_repo/.pre-commit-hooks.yaml | 10 +++ .../conda_hooks_repo/environment.yml | 6 ++ tests/repository_test.py | 40 +++++++++++ 8 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 pre_commit/languages/conda.py create mode 100644 pre_commit/resources/empty_template_environment.yml create mode 100644 testing/resources/conda_hooks_repo/.pre-commit-hooks.yaml create mode 100644 testing/resources/conda_hooks_repo/environment.yml diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 5b57e8948..9d61eb648 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -22,6 +22,9 @@ jobs: COVERAGE_IGNORE_WINDOWS: '# pragma: windows no cover' TOX_TESTENV_PASSENV: COVERAGE_IGNORE_WINDOWS TEMP: C:\Temp # remove when dropping python2 + pre_test: + - powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts" + displayName: Add conda to PATH - template: job--python-tox.yml@asottile parameters: toxenvs: [py37] diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 051656b7d..3d139d984 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +from pre_commit.languages import conda from pre_commit.languages import docker from pre_commit.languages import docker_image from pre_commit.languages import fail @@ -52,6 +53,7 @@ # """ languages = { + 'conda': conda, 'docker': docker, 'docker_image': docker_image, 'fail': fail, diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py new file mode 100644 index 000000000..a89d6c92b --- /dev/null +++ b/pre_commit/languages/conda.py @@ -0,0 +1,66 @@ +import contextlib +import os + +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import UNSET +from pre_commit.envcontext import Var +from pre_commit.languages import helpers +from pre_commit.util import clean_path_on_failure +from pre_commit.util import cmd_output_b + +ENVIRONMENT_DIR = 'conda' +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy + + +def get_env_patch(env): + # On non-windows systems executable live in $CONDA_PREFIX/bin, on Windows + # they can be in $CONDA_PREFIX/bin, $CONDA_PREFIX/Library/bin, + # $CONDA_PREFIX/Scripts and $CONDA_PREFIX. Whereas the latter only + # seems to be used for python.exe. + path = (os.path.join(env, 'bin'), os.pathsep, Var('PATH')) + if os.name == 'nt': # pragma: no cover (platform specific) + path = (env, os.pathsep) + path + path = (os.path.join(env, 'Scripts'), os.pathsep) + path + path = (os.path.join(env, 'Library', 'bin'), os.pathsep) + path + + return ( + ('PYTHONHOME', UNSET), + ('VIRTUAL_ENV', UNSET), + ('CONDA_PREFIX', env), + ('PATH', path), + ) + + +@contextlib.contextmanager +def in_env(prefix, language_version): + directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version) + envdir = prefix.path(directory) + with envcontext(get_env_patch(envdir)): + yield + + +def install_environment(prefix, version, additional_dependencies): + helpers.assert_version_default('conda', version) + directory = helpers.environment_dir(ENVIRONMENT_DIR, version) + + env_dir = prefix.path(directory) + with clean_path_on_failure(env_dir): + cmd_output_b( + 'conda', 'env', 'create', '-p', env_dir, '--file', + 'environment.yml', cwd=prefix.prefix_dir, + ) + if additional_dependencies: + cmd_output_b( + 'conda', 'install', '-p', env_dir, *additional_dependencies, + cwd=prefix.prefix_dir + ) + + +def run_hook(hook, file_args, color): + # TODO: Some rare commands need to be run using `conda run` but mostly we + # can run them withot which is much quicker and produces a better + # output. + # cmd = ('conda', 'run', '-p', env_dir) + hook.cmd + with in_env(hook.prefix, hook.language_version): + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/resources/empty_template_environment.yml b/pre_commit/resources/empty_template_environment.yml new file mode 100644 index 000000000..0f29f0c0a --- /dev/null +++ b/pre_commit/resources/empty_template_environment.yml @@ -0,0 +1,9 @@ +channels: + - conda-forge + - defaults +dependencies: + # This cannot be empty as otherwise no environment will be created. + # We're using openssl here as it is available on all system and will + # most likely be always installed anyways. + # See https://github.com/conda/conda/issues/9487 + - openssl diff --git a/pre_commit/store.py b/pre_commit/store.py index 2f1592444..d9b674b27 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -173,7 +173,7 @@ def _git_cmd(*args): LOCAL_RESOURCES = ( 'Cargo.toml', 'main.go', 'main.rs', '.npmignore', 'package.json', - 'pre_commit_dummy_package.gemspec', 'setup.py', + 'pre_commit_dummy_package.gemspec', 'setup.py', 'environment.yml', ) def make_local(self, deps): diff --git a/testing/resources/conda_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/conda_hooks_repo/.pre-commit-hooks.yaml new file mode 100644 index 000000000..a0d274c23 --- /dev/null +++ b/testing/resources/conda_hooks_repo/.pre-commit-hooks.yaml @@ -0,0 +1,10 @@ +- id: sys-exec + name: sys-exec + entry: python -c 'import os; import sys; print(sys.executable.split(os.path.sep)[-2]) if os.name == "nt" else print(sys.executable.split(os.path.sep)[-3])' + language: conda + files: \.py$ +- id: additional-deps + name: additional-deps + entry: python + language: conda + files: \.py$ diff --git a/testing/resources/conda_hooks_repo/environment.yml b/testing/resources/conda_hooks_repo/environment.yml new file mode 100644 index 000000000..e23c079fd --- /dev/null +++ b/testing/resources/conda_hooks_repo/environment.yml @@ -0,0 +1,6 @@ +channels: + - conda-forge + - defaults +dependencies: + - python + - pip diff --git a/tests/repository_test.py b/tests/repository_test.py index 85afa90d3..5f2ed1cb9 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -79,6 +79,46 @@ def _test_hook_repo( assert _norm_out(out) == expected +def test_conda_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'conda_hooks_repo', + 'sys-exec', [os.devnull], + b'conda-default\n', + ) + + +def test_conda_with_additional_dependencies_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'conda_hooks_repo', + 'additional-deps', [os.devnull], + b'OK\n', + config_kwargs={ + 'hooks': [{ + 'id': 'additional-deps', + 'args': ['-c', 'import mccabe; print("OK")'], + 'additional_dependencies': ['mccabe'], + }], + }, + ) + + +def test_local_conda_additional_dependencies(store): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'local-conda', + 'name': 'local-conda', + 'entry': 'python', + 'language': 'conda', + 'args': ['-c', 'import mccabe; print("OK")'], + 'additional_dependencies': ['mccabe'], + }], + } + ret, out = _get_hook(config, store, 'local-conda').run((), color=False) + assert ret == 0 + assert _norm_out(out) == b'OK\n' + + def test_python_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'python_hooks_repo', From 6af0e33eed78c420ed9bb077357d1f844d70c443 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 23 Dec 2019 12:04:05 -0800 Subject: [PATCH 498/544] Add top-level `files` key for inclusion --- pre_commit/clientlib.py | 14 +++++++------- pre_commit/commands/run.py | 4 +++- tests/commands/run_test.py | 16 ++++++++++++++++ 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 14a22b990..c4768ff31 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -18,6 +18,8 @@ logger = logging.getLogger('pre_commit') +check_string_regex = cfgv.check_and(cfgv.check_string, cfgv.check_regex) + def check_type_tag(tag): if tag not in ALL_TAGS: @@ -53,12 +55,8 @@ def _make_argparser(filenames_help): cfgv.Required('language', cfgv.check_one_of(all_languages)), cfgv.Optional('alias', cfgv.check_string, ''), - cfgv.Optional( - 'files', cfgv.check_and(cfgv.check_string, cfgv.check_regex), '', - ), - cfgv.Optional( - 'exclude', cfgv.check_and(cfgv.check_string, cfgv.check_regex), '^$', - ), + cfgv.Optional('files', check_string_regex, ''), + cfgv.Optional('exclude', check_string_regex, '^$'), cfgv.Optional('types', cfgv.check_array(check_type_tag), ['file']), cfgv.Optional('exclude_types', cfgv.check_array(check_type_tag), []), @@ -260,7 +258,8 @@ def warn_unknown_keys_repo(extra, orig_keys, dct): cfgv.check_array(cfgv.check_one_of(C.STAGES)), C.STAGES, ), - cfgv.Optional('exclude', cfgv.check_regex, '^$'), + cfgv.Optional('files', check_string_regex, ''), + cfgv.Optional('exclude', check_string_regex, '^$'), cfgv.Optional('fail_fast', cfgv.check_bool, False), cfgv.Optional( 'minimum_pre_commit_version', @@ -272,6 +271,7 @@ def warn_unknown_keys_repo(extra, orig_keys, dct): 'repos', 'default_language_version', 'default_stages', + 'files', 'exclude', 'fail_fast', 'minimum_pre_commit_version', diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 0b1f7b7ea..f5a5b1e62 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -206,7 +206,9 @@ def _run_hooks(config, hooks, args, environ): skips = _get_skips(environ) cols = _compute_cols(hooks, args.verbose) filenames = _all_filenames(args) - filenames = filter_by_include_exclude(filenames, '', config['exclude']) + filenames = filter_by_include_exclude( + filenames, config['files'], config['exclude'], + ) classifier = Classifier(filenames) retval = 0 for hook in hooks: diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 4221134bc..63d092547 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -180,6 +180,22 @@ def test_global_exclude(cap_out, store, tempdir_factory): assert printed.endswith(expected) +def test_global_files(cap_out, store, tempdir_factory): + git_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + with cwd(git_path): + with modify_config() as config: + config['files'] = '^bar.py$' + open('foo.py', 'a').close() + open('bar.py', 'a').close() + cmd_output('git', 'add', '.') + opts = run_opts(verbose=True) + ret, printed = _do_run(cap_out, store, git_path, opts) + assert ret == 0 + # Does not contain foo.py since it was not included + expected = b'hookid: bash_hook\n\nbar.py\nHello World\n\n' + assert printed.endswith(expected) + + @pytest.mark.parametrize( ('args', 'expected_out'), [ From 01a628d96d18551775bdb1859261ddcd680f9654 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 23 Dec 2019 15:00:31 -0800 Subject: [PATCH 499/544] Make verbose output less special --- pre_commit/color.py | 1 + pre_commit/commands/run.py | 106 ++++++++++------------- pre_commit/xargs.py | 14 +-- tests/commands/install_uninstall_test.py | 25 +++--- tests/commands/run_test.py | 24 +++-- tests/commands/try_repo_test.py | 25 +++--- tests/repository_test.py | 2 +- tests/xargs_test.py | 4 + 8 files changed, 96 insertions(+), 105 deletions(-) diff --git a/pre_commit/color.py b/pre_commit/color.py index 1fb6acceb..7a138f47f 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -15,6 +15,7 @@ GREEN = '\033[42m' YELLOW = '\033[43;30m' TURQUOISE = '\033[46;30m' +SUBTLE = '\033[2m' NORMAL = '\033[0m' diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index f5a5b1e62..4ea55ffc5 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -4,7 +4,6 @@ import os import re import subprocess -import sys from identify.identify import tags_from_path @@ -71,15 +70,15 @@ def _get_skips(environ): return {skip.strip() for skip in skips.split(',') if skip.strip()} -def _hook_msg_start(hook, verbose): - return '{}{}'.format('[{}] '.format(hook.id) if verbose else '', hook.name) - - SKIPPED = 'Skipped' NO_FILES = '(no files to check)' -def _run_single_hook(classifier, hook, args, skips, cols, use_color): +def _subtle_line(s, use_color): + output.write_line(color.format_color(s, color.SUBTLE, use_color)) + + +def _run_single_hook(classifier, hook, skips, cols, verbose, use_color): filenames = classifier.filenames_for_hook(hook) if hook.language == 'pcre': @@ -93,92 +92,78 @@ def _run_single_hook(classifier, hook, args, skips, cols, use_color): if hook.id in skips or hook.alias in skips: output.write( get_hook_message( - _hook_msg_start(hook, args.verbose), + hook.name, end_msg=SKIPPED, end_color=color.YELLOW, - use_color=args.color, + use_color=use_color, cols=cols, ), ) - return 0 + retcode = 0 + files_modified = False + out = b'' elif not filenames and not hook.always_run: output.write( get_hook_message( - _hook_msg_start(hook, args.verbose), + hook.name, postfix=NO_FILES, end_msg=SKIPPED, end_color=color.TURQUOISE, - use_color=args.color, + use_color=use_color, cols=cols, ), ) - return 0 - - # Print the hook and the dots first in case the hook takes hella long to - # run. - output.write( - get_hook_message( - _hook_msg_start(hook, args.verbose), end_len=6, cols=cols, - ), - ) - sys.stdout.flush() + retcode = 0 + files_modified = False + out = b'' + else: + # print hook and dots first in case the hook takes a while to run + output.write(get_hook_message(hook.name, end_len=6, cols=cols)) - diff_before = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None) - filenames = tuple(filenames) if hook.pass_filenames else () - retcode, out = hook.run(filenames, use_color) - diff_after = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None) + diff_cmd = ('git', 'diff', '--no-ext-diff') + diff_before = cmd_output_b(*diff_cmd, retcode=None) + filenames = tuple(filenames) if hook.pass_filenames else () + retcode, out = hook.run(filenames, use_color) + diff_after = cmd_output_b(*diff_cmd, retcode=None) - file_modifications = diff_before != diff_after + # if the hook makes changes, fail the commit + files_modified = diff_before != diff_after - # If the hook makes changes, fail the commit - if file_modifications: - retcode = 1 + if retcode or files_modified: + print_color = color.RED + status = 'Failed' + else: + print_color = color.GREEN + status = 'Passed' - if retcode: - retcode = 1 - print_color = color.RED - pass_fail = 'Failed' - else: - retcode = 0 - print_color = color.GREEN - pass_fail = 'Passed' + output.write_line(color.format_color(status, print_color, use_color)) - output.write_line(color.format_color(pass_fail, print_color, args.color)) + if verbose or hook.verbose or retcode or files_modified: + _subtle_line('- hook id: {}'.format(hook.id), use_color) - if ( - (out or file_modifications) and - (retcode or args.verbose or hook.verbose) - ): - output.write_line('hookid: {}\n'.format(hook.id)) + if retcode: + _subtle_line('- exit code: {}'.format(retcode), use_color) # Print a message if failing due to file modifications - if file_modifications: - output.write('Files were modified by this hook.') - - if out: - output.write_line(' Additional output:') - - output.write_line() + if files_modified: + _subtle_line('- files were modified by this hook', use_color) if out.strip(): + output.write_line() output.write_line(out.strip(), logfile_name=hook.log_file) - output.write_line() + output.write_line() - return retcode + return files_modified or bool(retcode) -def _compute_cols(hooks, verbose): +def _compute_cols(hooks): """Compute the number of columns to display hook messages. The widest that will be displayed is in the no files skipped case: Hook name...(no files to check) Skipped - - or in the verbose case - - Hook name [hookid]...(no files to check) Skipped """ if hooks: - name_len = max(len(_hook_msg_start(hook, verbose)) for hook in hooks) + name_len = max(len(hook.name) for hook in hooks) else: name_len = 0 @@ -204,7 +189,7 @@ def _all_filenames(args): def _run_hooks(config, hooks, args, environ): """Actually run the hooks.""" skips = _get_skips(environ) - cols = _compute_cols(hooks, args.verbose) + cols = _compute_cols(hooks) filenames = _all_filenames(args) filenames = filter_by_include_exclude( filenames, config['files'], config['exclude'], @@ -213,7 +198,8 @@ def _run_hooks(config, hooks, args, environ): retval = 0 for hook in hooks: retval |= _run_single_hook( - classifier, hook, args, skips, cols, args.color, + classifier, hook, skips, cols, + verbose=args.verbose, use_color=args.color, ) if retval and config['fail_fast']: break diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index f48f31364..5e405903b 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -135,17 +135,9 @@ def run_cmd_partition(run_cmd): results = thread_map(run_cmd_partition, partitions) for proc_retcode, proc_out, _ in results: - # This is *slightly* too clever so I'll explain it. - # First the xor boolean table: - # T | F | - # +-------+ - # T | F | T | - # --+-------+ - # F | T | F | - # --+-------+ - # When negate is True, it has the effect of flipping the return - # code. Otherwise, the returncode is unchanged. - retcode |= bool(proc_retcode) ^ negate + if negate: + proc_retcode = not proc_retcode + retcode = max(retcode, proc_retcode) stdout += proc_out return retcode, stdout diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 52f6e4e57..28bf66d1d 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -288,7 +288,8 @@ def test_environment_not_sourced(tempdir_factory, store): FAILING_PRE_COMMIT_RUN = re.compile( r'^\[INFO\] Initializing environment for .+\.\r?\n' r'Failing hook\.+Failed\r?\n' - r'hookid: failing_hook\r?\n' + r'- hook id: failing_hook\r?\n' + r'- exit code: 1\r?\n' r'\r?\n' r'Fail\r?\n' r'foo\r?\n' @@ -548,7 +549,7 @@ def test_pre_push_integration_failing(tempdir_factory, store): assert 'Failing hook' in output assert 'Failed' in output assert 'foo zzz' in output # both filenames should be printed - assert 'hookid: failing_hook' in output + assert 'hook id: failing_hook' in output def test_pre_push_integration_accepted(tempdir_factory, store): @@ -647,8 +648,11 @@ def test_commit_msg_integration_failing( install(C.CONFIG_FILE, store, hook_types=['commit-msg']) retc, out = _get_commit_output(tempdir_factory) assert retc == 1 - assert out.startswith('Must have "Signed off by:"...') - assert out.strip().endswith('...Failed') + assert out.replace('\r', '') == '''\ +Must have "Signed off by:"...............................................Failed +- hook id: must-have-signoff +- exit code: 1 +''' def test_commit_msg_integration_passing( @@ -691,16 +695,18 @@ def test_prepare_commit_msg_integration_failing( install(C.CONFIG_FILE, store, hook_types=['prepare-commit-msg']) retc, out = _get_commit_output(tempdir_factory) assert retc == 1 - assert out.startswith('Add "Signed off by:"...') - assert out.strip().endswith('...Failed') + assert out.replace('\r', '') == '''\ +Add "Signed off by:".....................................................Failed +- hook id: add-signoff +- exit code: 1 +''' def test_prepare_commit_msg_integration_passing( prepare_commit_msg_repo, tempdir_factory, store, ): install(C.CONFIG_FILE, store, hook_types=['prepare-commit-msg']) - msg = 'Hi' - retc, out = _get_commit_output(tempdir_factory, msg=msg) + retc, out = _get_commit_output(tempdir_factory, msg='Hi') assert retc == 0 first_line = out.splitlines()[0] assert first_line.startswith('Add "Signed off by:"...') @@ -730,8 +736,7 @@ def test_prepare_commit_msg_legacy( install(C.CONFIG_FILE, store, hook_types=['prepare-commit-msg']) - msg = 'Hi' - retc, out = _get_commit_output(tempdir_factory, msg=msg) + retc, out = _get_commit_output(tempdir_factory, msg='Hi') assert retc == 0 first_line, second_line = out.splitlines()[:2] assert first_line == 'legacy' diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 63d092547..4c75e62a9 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -94,7 +94,7 @@ def test_run_all_hooks_failing(cap_out, store, repo_with_failing_hook): ( b'Failing hook', b'Failed', - b'hookid: failing_hook', + b'hook id: failing_hook', b'Fail\nfoo.py\n', ), expected_ret=1, @@ -125,14 +125,14 @@ def test_hook_that_modifies_but_returns_zero(cap_out, store, tempdir_factory): # The first should fail b'Failed', # With a modified file (default message + the hook's output) - b'Files were modified by this hook. Additional output:\n\n' + b'- files were modified by this hook\n\n' b'Modified: foo.py', # The next hook should pass despite the first modifying b'Passed', # The next hook should fail b'Failed', # bar.py was modified, but provides no additional output - b'Files were modified by this hook.\n', + b'- files were modified by this hook\n', ), 1, True, @@ -176,7 +176,7 @@ def test_global_exclude(cap_out, store, tempdir_factory): ret, printed = _do_run(cap_out, store, git_path, opts) assert ret == 0 # Does not contain foo.py since it was excluded - expected = b'hookid: bash_hook\n\nbar.py\nHello World\n\n' + expected = b'- hook id: bash_hook\n\nbar.py\nHello World\n\n' assert printed.endswith(expected) @@ -192,7 +192,7 @@ def test_global_files(cap_out, store, tempdir_factory): ret, printed = _do_run(cap_out, store, git_path, opts) assert ret == 0 # Does not contain foo.py since it was not included - expected = b'hookid: bash_hook\n\nbar.py\nHello World\n\n' + expected = b'- hook id: bash_hook\n\nbar.py\nHello World\n\n' assert printed.endswith(expected) @@ -422,23 +422,21 @@ def test_merge_conflict_resolved(cap_out, store, in_merge_conflict): @pytest.mark.parametrize( - ('hooks', 'verbose', 'expected'), + ('hooks', 'expected'), ( - ([], True, 80), - ([auto_namedtuple(id='a', name='a' * 51)], False, 81), - ([auto_namedtuple(id='a', name='a' * 51)], True, 85), + ([], 80), + ([auto_namedtuple(id='a', name='a' * 51)], 81), ( [ auto_namedtuple(id='a', name='a' * 51), auto_namedtuple(id='b', name='b' * 52), ], - False, 82, ), ), ) -def test_compute_cols(hooks, verbose, expected): - assert _compute_cols(hooks, verbose) == expected +def test_compute_cols(hooks, expected): + assert _compute_cols(hooks) == expected @pytest.mark.parametrize( @@ -492,7 +490,7 @@ def test_hook_id_in_verbose_output(cap_out, store, repo_with_passing_hook): ret, printed = _do_run( cap_out, store, repo_with_passing_hook, run_opts(verbose=True), ) - assert b'[bash_hook] Bash hook' in printed + assert b'- hook id: bash_hook' in printed def test_multiple_hooks_same_id(cap_out, store, repo_with_passing_hook): diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index d9a0401ae..6e9db9dbb 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -54,15 +54,17 @@ def test_try_repo_repo_only(cap_out, tempdir_factory): ' - id: bash_hook3\n$', config, ) - assert rest == ( - '[bash_hook] Bash hook................................(no files to check)Skipped\n' # noqa: E501 - '[bash_hook2] Bash hook...................................................Passed\n' # noqa: E501 - 'hookid: bash_hook2\n' - '\n' - 'test-file\n' - '\n' - '[bash_hook3] Bash hook...............................(no files to check)Skipped\n' # noqa: E501 - ) + assert rest == '''\ +Bash hook............................................(no files to check)Skipped +- hook id: bash_hook +Bash hook................................................................Passed +- hook id: bash_hook2 + +test-file + +Bash hook............................................(no files to check)Skipped +- hook id: bash_hook3 +''' def test_try_repo_with_specific_hook(cap_out, tempdir_factory): @@ -77,7 +79,10 @@ def test_try_repo_with_specific_hook(cap_out, tempdir_factory): ' - id: bash_hook\n$', config, ) - assert rest == '[bash_hook] Bash hook................................(no files to check)Skipped\n' # noqa: E501 + assert rest == '''\ +Bash hook............................................(no files to check)Skipped +- hook id: bash_hook +''' def test_try_repo_relative_path(cap_out, tempdir_factory): diff --git a/tests/repository_test.py b/tests/repository_test.py index 5f2ed1cb9..8f001384d 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -221,7 +221,7 @@ def test_run_a_failing_docker_hook(tempdir_factory, store): 'docker-hook-failing', ['Hello World from docker'], mock.ANY, # an error message about `bork` not existing - expected_return_code=1, + expected_return_code=127, ) diff --git a/tests/xargs_test.py b/tests/xargs_test.py index a6772804a..65b1d495b 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -178,6 +178,10 @@ def test_xargs_retcode_normal(): ret, _ = xargs.xargs(exit_cmd, ('0', '1'), _max_length=max_length) assert ret == 1 + # takes the maximum return code + ret, _ = xargs.xargs(exit_cmd, ('0', '5', '1'), _max_length=max_length) + assert ret == 5 + def test_xargs_concurrency(): bash_cmd = parse_shebang.normalize_cmd(('bash', '-c')) From b90412742e9b91d60b0fcdc59e103f1a9220695b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 23 Dec 2019 17:46:39 -0800 Subject: [PATCH 500/544] A few cleanups for CalledProcessError to hopefully make it more readable --- pre_commit/util.py | 39 ++++++++++++++-------------------- tests/languages/docker_test.py | 2 +- tests/store_test.py | 2 +- tests/util_test.py | 38 ++++++++++++++++----------------- 4 files changed, 36 insertions(+), 45 deletions(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index 0f54e9e1e..8072042b9 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -74,36 +74,31 @@ def make_executable(filename): class CalledProcessError(RuntimeError): - def __init__(self, returncode, cmd, expected_returncode, output=None): + def __init__(self, returncode, cmd, expected_returncode, stdout, stderr): super(CalledProcessError, self).__init__( - returncode, cmd, expected_returncode, output, + returncode, cmd, expected_returncode, stdout, stderr, ) self.returncode = returncode self.cmd = cmd self.expected_returncode = expected_returncode - self.output = output + self.stdout = stdout + self.stderr = stderr def to_bytes(self): - output = [] - for maybe_text in self.output: - if maybe_text: - output.append( - b'\n ' + - five.to_bytes(maybe_text).replace(b'\n', b'\n '), - ) + def _indent_or_none(part): + if part: + return b'\n ' + part.replace(b'\n', b'\n ') else: - output.append(b'(none)') + return b' (none)' return b''.join(( - five.to_bytes( - 'Command: {!r}\n' - 'Return code: {}\n' - 'Expected return code: {}\n'.format( - self.cmd, self.returncode, self.expected_returncode, - ), - ), - b'Output: ', output[0], b'\n', - b'Errors: ', output[1], + 'command: {!r}\n' + 'return code: {}\n' + 'expected return code: {}\n'.format( + self.cmd, self.returncode, self.expected_returncode, + ).encode('UTF-8'), + b'stdout:', _indent_or_none(self.stdout), b'\n', + b'stderr:', _indent_or_none(self.stderr), )) def to_text(self): @@ -143,9 +138,7 @@ def cmd_output_b(*cmd, **kwargs): returncode = proc.returncode if retcode is not None and retcode != returncode: - raise CalledProcessError( - returncode, cmd, retcode, output=(stdout_b, stderr_b), - ) + raise CalledProcessError(returncode, cmd, retcode, stdout_b, stderr_b) return returncode, stdout_b, stderr_b diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 42616cdc5..4ea767917 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -10,7 +10,7 @@ def test_docker_is_running_process_error(): with mock.patch( 'pre_commit.languages.docker.cmd_output_b', - side_effect=CalledProcessError(*(None,) * 4), + side_effect=CalledProcessError(None, None, None, None, None), ): assert docker.docker_is_running() is False diff --git a/tests/store_test.py b/tests/store_test.py index 1833dee73..c71c35099 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -125,7 +125,7 @@ def test_clone_shallow_failure_fallback_to_complete( # Force shallow clone failure def fake_shallow_clone(self, *args, **kwargs): - raise CalledProcessError(None, None, None) + raise CalledProcessError(None, None, None, None, None) store._shallow_clone = fake_shallow_clone ret = store.clone(path, rev) diff --git a/tests/util_test.py b/tests/util_test.py index dd1ad37bd..647fd1870 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -9,6 +9,7 @@ from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_p from pre_commit.util import parse_version from pre_commit.util import rmtree @@ -16,30 +17,26 @@ def test_CalledProcessError_str(): - error = CalledProcessError( - 1, [str('git'), str('status')], 0, (str('stdout'), str('stderr')), - ) + error = CalledProcessError(1, [str('exe')], 0, b'output', b'errors') assert str(error) == ( - "Command: ['git', 'status']\n" - 'Return code: 1\n' - 'Expected return code: 0\n' - 'Output: \n' - ' stdout\n' - 'Errors: \n' - ' stderr' + "command: ['exe']\n" + 'return code: 1\n' + 'expected return code: 0\n' + 'stdout:\n' + ' output\n' + 'stderr:\n' + ' errors' ) def test_CalledProcessError_str_nooutput(): - error = CalledProcessError( - 1, [str('git'), str('status')], 0, (str(''), str('')), - ) + error = CalledProcessError(1, [str('exe')], 0, b'', b'') assert str(error) == ( - "Command: ['git', 'status']\n" - 'Return code: 1\n' - 'Expected return code: 0\n' - 'Output: (none)\n' - 'Errors: (none)' + "command: ['exe']\n" + 'return code: 1\n' + 'expected return code: 0\n' + 'stdout: (none)\n' + 'stderr: (none)' ) @@ -90,8 +87,9 @@ def test_cmd_output_exe_not_found(): assert out == 'Executable `dne` not found' -def test_cmd_output_p_exe_not_found(): - ret, out, _ = cmd_output_p('dne', retcode=None, stderr=subprocess.STDOUT) +@pytest.mark.parametrize('fn', (cmd_output_b, cmd_output_p)) +def test_cmd_output_exe_not_found_bytes(fn): + ret, out, _ = fn('dne', retcode=None, stderr=subprocess.STDOUT) assert ret == 1 assert out == b'Executable `dne` not found' From 4941ed58d5b73c7dc66fface056a0decd39808fc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 23 Dec 2019 18:27:30 -0800 Subject: [PATCH 501/544] Normalize crlf in tests --- testing/util.py | 9 +++- tests/commands/init_templatedir_test.py | 5 +- tests/commands/install_uninstall_test.py | 62 +++++++++++------------- tests/commands/run_test.py | 13 ++--- tests/commands/try_repo_test.py | 3 +- tests/conftest.py | 2 +- tests/error_handler_test.py | 10 ++-- 7 files changed, 46 insertions(+), 58 deletions(-) diff --git a/testing/util.py b/testing/util.py index d82612fa5..dde0c4d03 100644 --- a/testing/util.py +++ b/testing/util.py @@ -2,6 +2,7 @@ import contextlib import os.path +import subprocess import sys import pytest @@ -24,9 +25,11 @@ def cmd_output_mocked_pre_commit_home(*args, **kwargs): # keyword-only argument tempdir_factory = kwargs.pop('tempdir_factory') pre_commit_home = kwargs.pop('pre_commit_home', tempdir_factory.get()) + kwargs.setdefault('stderr', subprocess.STDOUT) # Don't want to write to the home directory env = dict(kwargs.pop('env', os.environ), PRE_COMMIT_HOME=pre_commit_home) - return cmd_output(*args, env=env, **kwargs) + ret, out, _ = cmd_output(*args, env=env, **kwargs) + return ret, out.replace('\r\n', '\n'), None skipif_cant_run_docker = pytest.mark.skipif( @@ -137,8 +140,10 @@ def cwd(path): def git_commit(*args, **kwargs): fn = kwargs.pop('fn', cmd_output) msg = kwargs.pop('msg', 'commit!') + kwargs.setdefault('stderr', subprocess.STDOUT) cmd = ('git', 'commit', '--allow-empty', '--no-gpg-sign', '-a') + args if msg is not None: # allow skipping `-m` with `msg=None` cmd += ('-m', msg) - return fn(*cmd, **kwargs) + ret, out, _ = fn(*cmd, **kwargs) + return ret, out.replace('\r\n', '\n') diff --git a/tests/commands/init_templatedir_test.py b/tests/commands/init_templatedir_test.py index 1bb9695fe..12c6696a8 100644 --- a/tests/commands/init_templatedir_test.py +++ b/tests/commands/init_templatedir_test.py @@ -1,5 +1,4 @@ import os.path -import subprocess import mock @@ -30,11 +29,9 @@ def test_init_templatedir(tmpdir, tempdir_factory, store, cap_out): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - retcode, output, _ = git_commit( + retcode, output = git_commit( fn=cmd_output_mocked_pre_commit_home, tempdir_factory=tempdir_factory, - # git commit puts pre-commit to stderr - stderr=subprocess.STDOUT, ) assert retcode == 0 assert 'Bash hook....' in output diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 28bf66d1d..ba6265178 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -5,7 +5,6 @@ import io import os.path import re -import subprocess import sys import mock @@ -121,30 +120,28 @@ def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): cmd_output('git', 'add', touch_file) return git_commit( fn=cmd_output_mocked_pre_commit_home, - # git commit puts pre-commit to stderr - stderr=subprocess.STDOUT, retcode=None, tempdir_factory=tempdir_factory, **kwargs - )[:2] + ) # osx does this different :( FILES_CHANGED = ( r'(' - r' 1 file changed, 0 insertions\(\+\), 0 deletions\(-\)\r?\n' + r' 1 file changed, 0 insertions\(\+\), 0 deletions\(-\)\n' r'|' - r' 0 files changed\r?\n' + r' 0 files changed\n' r')' ) NORMAL_PRE_COMMIT_RUN = re.compile( - r'^\[INFO\] Initializing environment for .+\.\r?\n' - r'Bash hook\.+Passed\r?\n' - r'\[master [a-f0-9]{7}\] commit!\r?\n' + + r'^\[INFO\] Initializing environment for .+\.\n' + r'Bash hook\.+Passed\n' + r'\[master [a-f0-9]{7}\] commit!\n' + FILES_CHANGED + - r' create mode 100644 foo\r?\n$', + r' create mode 100644 foo\n$', ) @@ -265,7 +262,7 @@ def test_environment_not_sourced(tempdir_factory, store): # Use a specific homedir to ignore --user installs homedir = tempdir_factory.get() - ret, stdout, stderr = git_commit( + ret, out = git_commit( env={ 'HOME': homedir, 'PATH': _path_without_us(), @@ -278,22 +275,21 @@ def test_environment_not_sourced(tempdir_factory, store): retcode=None, ) assert ret == 1 - assert stdout == '' - assert stderr.replace('\r\n', '\n') == ( + assert out == ( '`pre-commit` not found. ' 'Did you forget to activate your virtualenv?\n' ) FAILING_PRE_COMMIT_RUN = re.compile( - r'^\[INFO\] Initializing environment for .+\.\r?\n' - r'Failing hook\.+Failed\r?\n' - r'- hook id: failing_hook\r?\n' - r'- exit code: 1\r?\n' - r'\r?\n' - r'Fail\r?\n' - r'foo\r?\n' - r'\r?\n$', + r'^\[INFO\] Initializing environment for .+\.\n' + r'Failing hook\.+Failed\n' + r'- hook id: failing_hook\n' + r'- exit code: 1\n' + r'\n' + r'Fail\n' + r'foo\n' + r'\n$', ) @@ -308,10 +304,10 @@ def test_failing_hooks_returns_nonzero(tempdir_factory, store): EXISTING_COMMIT_RUN = re.compile( - r'^legacy hook\r?\n' - r'\[master [a-f0-9]{7}\] commit!\r?\n' + + r'^legacy hook\n' + r'\[master [a-f0-9]{7}\] commit!\n' + FILES_CHANGED + - r' create mode 100644 baz\r?\n$', + r' create mode 100644 baz\n$', ) @@ -369,9 +365,9 @@ def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory, store): FAIL_OLD_HOOK = re.compile( - r'fail!\r?\n' - r'\[INFO\] Initializing environment for .+\.\r?\n' - r'Bash hook\.+Passed\r?\n', + r'fail!\n' + r'\[INFO\] Initializing environment for .+\.\n' + r'Bash hook\.+Passed\n', ) @@ -465,10 +461,10 @@ def test_uninstall_doesnt_remove_not_our_hooks(in_git_dir): PRE_INSTALLED = re.compile( - r'Bash hook\.+Passed\r?\n' - r'\[master [a-f0-9]{7}\] commit!\r?\n' + + r'Bash hook\.+Passed\n' + r'\[master [a-f0-9]{7}\] commit!\n' + FILES_CHANGED + - r' create mode 100644 foo\r?\n$', + r' create mode 100644 foo\n$', ) @@ -527,8 +523,6 @@ def test_installed_from_venv(tempdir_factory, store): def _get_push_output(tempdir_factory, opts=()): return cmd_output_mocked_pre_commit_home( 'git', 'push', 'origin', 'HEAD:new_branch', *opts, - # git push puts pre-commit to stderr - stderr=subprocess.STDOUT, tempdir_factory=tempdir_factory, retcode=None )[:2] @@ -648,7 +642,7 @@ def test_commit_msg_integration_failing( install(C.CONFIG_FILE, store, hook_types=['commit-msg']) retc, out = _get_commit_output(tempdir_factory) assert retc == 1 - assert out.replace('\r', '') == '''\ + assert out == '''\ Must have "Signed off by:"...............................................Failed - hook id: must-have-signoff - exit code: 1 @@ -695,7 +689,7 @@ def test_prepare_commit_msg_integration_failing( install(C.CONFIG_FILE, store, hook_types=['prepare-commit-msg']) retc, out = _get_commit_output(tempdir_factory) assert retc == 1 - assert out.replace('\r', '') == '''\ + assert out == '''\ Add "Signed off by:".....................................................Failed - hook id: add-signoff - exit code: 1 diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 4c75e62a9..e56612e3f 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -4,7 +4,6 @@ import io import os.path import pipes -import subprocess import sys import mock @@ -543,16 +542,14 @@ def test_stdout_write_bug_py26(repo_with_failing_hook, store, tempdir_factory): install(C.CONFIG_FILE, store, hook_types=['pre-commit']) # Have to use subprocess because pytest monkeypatches sys.stdout - _, stdout, _ = git_commit( + _, out = git_commit( fn=cmd_output_mocked_pre_commit_home, - # git commit puts pre-commit to stderr - stderr=subprocess.STDOUT, - retcode=None, tempdir_factory=tempdir_factory, + retcode=None, ) - assert 'UnicodeEncodeError' not in stdout + assert 'UnicodeEncodeError' not in out # Doesn't actually happen, but a reasonable assertion - assert 'UnicodeDecodeError' not in stdout + assert 'UnicodeDecodeError' not in out def test_lots_of_files(store, tempdir_factory): @@ -574,8 +571,6 @@ def test_lots_of_files(store, tempdir_factory): git_commit( fn=cmd_output_mocked_pre_commit_home, - # git commit puts pre-commit to stderr - stderr=subprocess.STDOUT, tempdir_factory=tempdir_factory, ) diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index 6e9db9dbb..ee010636b 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -21,8 +21,7 @@ def try_repo_opts(repo, ref=None, **kwargs): def _get_out(cap_out): - out = cap_out.get().replace('\r\n', '\n') - out = re.sub(r'\[INFO\].+\n', '', out) + out = re.sub(r'\[INFO\].+\n', '', cap_out.get()) start, using_config, config, rest = out.split('=' * 79 + '\n') assert using_config == 'Using config:\n' return start, config, rest diff --git a/tests/conftest.py b/tests/conftest.py index 635ea39af..6e9fcf23c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -249,7 +249,7 @@ def get_bytes(self): data = self._stream.data.getvalue() self._stream.data.seek(0) self._stream.data.truncate() - return data + return data.replace(b'\r\n', b'\n') def get(self): """Get the output assuming it was written as UTF-8 bytes""" diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index ff311a24d..74ade6189 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -144,7 +144,7 @@ def test_error_handler_non_ascii_exception(mock_store_dir): def test_error_handler_no_tty(tempdir_factory): pre_commit_home = tempdir_factory.get() - output = cmd_output_mocked_pre_commit_home( + ret, out, _ = cmd_output_mocked_pre_commit_home( sys.executable, '-c', 'from __future__ import unicode_literals\n' @@ -156,8 +156,6 @@ def test_error_handler_no_tty(tempdir_factory): pre_commit_home=pre_commit_home, ) log_file = os.path.join(pre_commit_home, 'pre-commit.log') - output_lines = output[1].replace('\r', '').splitlines() - assert ( - output_lines[-2] == 'An unexpected error has occurred: ValueError: ☃' - ) - assert output_lines[-1] == 'Check the log at {}'.format(log_file) + out_lines = out.splitlines() + assert out_lines[-2] == 'An unexpected error has occurred: ValueError: ☃' + assert out_lines[-1] == 'Check the log at {}'.format(log_file) From 8c93896c48c97900b9f0357e833e01f010b444a0 Mon Sep 17 00:00:00 2001 From: Ivan Gankevich Date: Thu, 26 Dec 2019 12:43:55 +0300 Subject: [PATCH 502/544] Add GIT_SSL_CAINFO environment variable to whitelist. This commit fixes #1253. --- pre_commit/git.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 3ee9ca3af..c8faf60f7 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -32,7 +32,7 @@ def no_git_env(_env=None): return { k: v for k, v in _env.items() if not k.startswith('GIT_') or - k in {'GIT_EXEC_PATH', 'GIT_SSH', 'GIT_SSH_COMMAND'} + k in {'GIT_EXEC_PATH', 'GIT_SSH', 'GIT_SSH_COMMAND', 'GIT_SSL_CAINFO'} } From c699e255a166bcb785e511faf1f0c4701cfef6ba Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 6 Nov 2019 19:13:12 -0800 Subject: [PATCH 503/544] support pre-merge-commit --- pre_commit/constants.py | 5 ++++- pre_commit/main.py | 3 ++- pre_commit/resources/hook-tmpl | 1 + testing/util.py | 5 ++--- tests/commands/install_uninstall_test.py | 28 ++++++++++++++++++++++++ tests/repository_test.py | 3 ++- 6 files changed, 39 insertions(+), 6 deletions(-) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 7dd447c0f..3aa452c40 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -26,6 +26,9 @@ VERSION = importlib_metadata.version('pre_commit') # `manual` is not invoked by any installed git hook. See #719 -STAGES = ('commit', 'prepare-commit-msg', 'commit-msg', 'manual', 'push') +STAGES = ( + 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', 'manual', + 'push', +) DEFAULT = 'default' diff --git a/pre_commit/main.py b/pre_commit/main.py index 686ddc4c2..fe1beafdf 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -70,7 +70,8 @@ def __call__(self, parser, namespace, values, option_string=None): def _add_hook_type_option(parser): parser.add_argument( '-t', '--hook-type', choices=( - 'pre-commit', 'pre-push', 'prepare-commit-msg', 'commit-msg', + 'pre-commit', 'pre-merge-commit', 'pre-push', + 'prepare-commit-msg', 'commit-msg', ), action=AppendReplaceDefault, default=['pre-commit'], diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index a145c8ee8..81ffc955c 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -163,6 +163,7 @@ def _opts(stdin): fns = { 'prepare-commit-msg': lambda _: ('--commit-msg-filename', sys.argv[1]), 'commit-msg': lambda _: ('--commit-msg-filename', sys.argv[1]), + 'pre-merge-commit': lambda _: (), 'pre-commit': lambda _: (), 'pre-push': _pre_push, } diff --git a/testing/util.py b/testing/util.py index dde0c4d03..600f1c593 100644 --- a/testing/util.py +++ b/testing/util.py @@ -36,16 +36,15 @@ def cmd_output_mocked_pre_commit_home(*args, **kwargs): os.name == 'nt' or not docker_is_running(), reason="Docker isn't running or can't be accessed", ) - skipif_cant_run_swift = pytest.mark.skipif( parse_shebang.find_executable('swift') is None, - reason='swift isn\'t installed or can\'t be found', + reason="swift isn't installed or can't be found", ) - xfailif_windows_no_ruby = pytest.mark.xfail( os.name == 'nt', reason='Ruby support not yet implemented on windows.', ) +xfailif_windows = pytest.mark.xfail(os.name == 'nt', reason='windows') def broken_deep_listdir(): # pragma: no cover (platform specific) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index ba6265178..f0e170973 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -29,6 +29,7 @@ from testing.util import cwd from testing.util import git_commit from testing.util import xfailif_no_symlink +from testing.util import xfailif_windows def test_is_not_script(): @@ -742,6 +743,33 @@ def test_prepare_commit_msg_legacy( assert 'Signed off by: ' in f.read() +@xfailif_windows # pragma: windows no cover (once AP has git 2.24) +def test_pre_merge_commit_integration(tempdir_factory, store): + expected = re.compile( + r'^\[INFO\] Initializing environment for .+\n' + r'Bash hook\.+Passed\n' + r"Merge made by the 'recursive' strategy.\n" + r' foo \| 0\n' + r' 1 file changed, 0 insertions\(\+\), 0 deletions\(-\)\n' + r' create mode 100644 foo\n$', + ) + + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + with cwd(path): + ret = install(C.CONFIG_FILE, store, hook_types=['pre-merge-commit']) + assert ret == 0 + + cmd_output('git', 'checkout', 'master', '-b', 'feature') + _get_commit_output(tempdir_factory) + cmd_output('git', 'checkout', 'master') + ret, output, _ = cmd_output_mocked_pre_commit_home( + 'git', 'merge', '--no-ff', '--no-edit', 'feature', + tempdir_factory=tempdir_factory, + ) + assert ret == 0 + assert expected.match(output) + + def test_install_disallow_missing_config(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): diff --git a/tests/repository_test.py b/tests/repository_test.py index 8f001384d..a468e707c 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -895,7 +895,8 @@ def test_manifest_hooks(tempdir_factory, store): pass_filenames=True, require_serial=False, stages=( - 'commit', 'prepare-commit-msg', 'commit-msg', 'manual', 'push', + 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', + 'manual', 'push', ), types=['file'], verbose=False, From 8a3c740f9e9934a7800fff701cad478e53e9c626 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Dec 2019 12:25:30 -0800 Subject: [PATCH 504/544] Implement `pre-commit autoupdate --freeze` --- pre_commit/clientlib.py | 3 +- pre_commit/commands/autoupdate.py | 183 +++++++-------- pre_commit/main.py | 5 + tests/commands/autoupdate_test.py | 375 +++++++++++++++++------------- tests/commands/gc_test.py | 2 +- 5 files changed, 311 insertions(+), 257 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index c4768ff31..74a37a8f3 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -133,8 +133,7 @@ def apply_default(self, dct): if 'sha' in dct: dct['rev'] = dct.pop('sha') - def remove_default(self, dct): - pass + remove_default = cfgv.Required.remove_default def _entry(modname): diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index d56a88fb3..eea5be7ca 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -1,18 +1,17 @@ from __future__ import print_function from __future__ import unicode_literals +import collections import os.path import re import six from aspy.yaml import ordered_dump from aspy.yaml import ordered_load -from cfgv import remove_defaults import pre_commit.constants as C from pre_commit import git from pre_commit import output -from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import InvalidManifestError from pre_commit.clientlib import load_config from pre_commit.clientlib import load_manifest @@ -25,39 +24,44 @@ from pre_commit.util import tmpdir -class RepositoryCannotBeUpdatedError(RuntimeError): - pass - +class RevInfo(collections.namedtuple('RevInfo', ('repo', 'rev', 'frozen'))): + __slots__ = () -def _update_repo(repo_config, store, tags_only): - """Updates a repository to the tip of `master`. If the repository cannot - be updated because a hook that is configured does not exist in `master`, - this raises a RepositoryCannotBeUpdatedError + @classmethod + def from_config(cls, config): + return cls(config['repo'], config['rev'], None) - Args: - repo_config - A config for a repository - """ - with tmpdir() as repo_path: - git.init_repo(repo_path, repo_config['repo']) - cmd_output_b('git', 'fetch', 'origin', 'HEAD', '--tags', cwd=repo_path) - - tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags') + def update(self, tags_only, freeze): if tags_only: - tag_cmd += ('--abbrev=0',) + tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags', '--abbrev=0') else: - tag_cmd += ('--exact',) - try: - rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip() - except CalledProcessError: - tag_cmd = ('git', 'rev-parse', 'FETCH_HEAD') - rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip() + tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags', '--exact') + + with tmpdir() as tmp: + git.init_repo(tmp, self.repo) + cmd_output_b('git', 'fetch', 'origin', 'HEAD', '--tags', cwd=tmp) + + try: + rev = cmd_output(*tag_cmd, cwd=tmp)[1].strip() + except CalledProcessError: + cmd = ('git', 'rev-parse', 'FETCH_HEAD') + rev = cmd_output(*cmd, cwd=tmp)[1].strip() + + frozen = None + if freeze: + exact = cmd_output('git', 'rev-parse', rev, cwd=tmp)[1].strip() + if exact != rev: + rev, frozen = exact, rev + return self._replace(rev=rev, frozen=frozen) + + +class RepositoryCannotBeUpdatedError(RuntimeError): + pass - # Don't bother trying to update if our rev is the same - if rev == repo_config['rev']: - return repo_config +def _check_hooks_still_exist_at_rev(repo_config, info, store): try: - path = store.clone(repo_config['repo'], rev) + path = store.clone(repo_config['repo'], info.rev) manifest = load_manifest(os.path.join(path, C.MANIFEST_FILE)) except InvalidManifestError as e: raise RepositoryCannotBeUpdatedError(six.text_type(e)) @@ -71,94 +75,91 @@ def _update_repo(repo_config, store, tags_only): '{}'.format(', '.join(sorted(hooks_missing))), ) - # Construct a new config with the head rev - new_config = repo_config.copy() - new_config['rev'] = rev - return new_config - -REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([^\s#]+)(.*)$', re.DOTALL) -REV_LINE_FMT = '{}rev:{}{}{}' +REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([^\s#]+)(.*)(\r?\n)$', re.DOTALL) +REV_LINE_FMT = '{}rev:{}{}{}{}' -def _write_new_config_file(path, output): +def _original_lines(path, rev_infos, retry=False): + """detect `rev:` lines or reformat the file""" with open(path) as f: - original_contents = f.read() - output = remove_defaults(output, CONFIG_SCHEMA) - new_contents = ordered_dump(output, **C.YAML_DUMP_KWARGS) - - lines = original_contents.splitlines(True) - rev_line_indices_reversed = list( - reversed([ - i for i, line in enumerate(lines) if REV_LINE_RE.match(line) - ]), - ) - - for line in new_contents.splitlines(True): - if REV_LINE_RE.match(line): - # It's possible we didn't identify the rev lines in the original - if not rev_line_indices_reversed: - break - line_index = rev_line_indices_reversed.pop() - original_line = lines[line_index] - orig_match = REV_LINE_RE.match(original_line) - new_match = REV_LINE_RE.match(line) - lines[line_index] = REV_LINE_FMT.format( - orig_match.group(1), orig_match.group(2), - new_match.group(3), orig_match.group(4), - ) - - # If we failed to intelligently rewrite the rev lines, fall back to the - # pretty-formatted yaml output - to_write = ''.join(lines) - if remove_defaults(ordered_load(to_write), CONFIG_SCHEMA) != output: - to_write = new_contents + original = f.read() + + lines = original.splitlines(True) + idxs = [i for i, line in enumerate(lines) if REV_LINE_RE.match(line)] + if len(idxs) == len(rev_infos): + return lines, idxs + elif retry: + raise AssertionError('could not find rev lines') + else: + with open(path, 'w') as f: + f.write(ordered_dump(ordered_load(original), **C.YAML_DUMP_KWARGS)) + return _original_lines(path, rev_infos, retry=True) + + +def _write_new_config(path, rev_infos): + lines, idxs = _original_lines(path, rev_infos) + + for idx, rev_info in zip(idxs, rev_infos): + if rev_info is None: + continue + match = REV_LINE_RE.match(lines[idx]) + assert match is not None + new_rev_s = ordered_dump({'rev': rev_info.rev}, **C.YAML_DUMP_KWARGS) + new_rev = new_rev_s.split(':', 1)[1].strip() + if rev_info.frozen is not None: + comment = ' # {}'.format(rev_info.frozen) + else: + comment = match.group(4) + lines[idx] = REV_LINE_FMT.format( + match.group(1), match.group(2), new_rev, comment, match.group(5), + ) with open(path, 'w') as f: - f.write(to_write) + f.write(''.join(lines)) -def autoupdate(config_file, store, tags_only, repos=()): +def autoupdate(config_file, store, tags_only, freeze, repos=()): """Auto-update the pre-commit config to the latest versions of repos.""" migrate_config(config_file, quiet=True) retv = 0 - output_repos = [] + rev_infos = [] changed = False - input_config = load_config(config_file) + config = load_config(config_file) + for repo_config in config['repos']: + if repo_config['repo'] in {LOCAL, META}: + continue - for repo_config in input_config['repos']: - if ( - repo_config['repo'] in {LOCAL, META} or - # Skip updating any repo_configs that aren't for the specified repo - repos and repo_config['repo'] not in repos - ): - output_repos.append(repo_config) + info = RevInfo.from_config(repo_config) + if repos and info.repo not in repos: + rev_infos.append(None) continue - output.write('Updating {}...'.format(repo_config['repo'])) + + output.write('Updating {}...'.format(info.repo)) + new_info = info.update(tags_only=tags_only, freeze=freeze) try: - new_repo_config = _update_repo(repo_config, store, tags_only) + _check_hooks_still_exist_at_rev(repo_config, new_info, store) except RepositoryCannotBeUpdatedError as error: output.write_line(error.args[0]) - output_repos.append(repo_config) + rev_infos.append(None) retv = 1 continue - if new_repo_config['rev'] != repo_config['rev']: + if new_info.rev != info.rev: changed = True - output.write_line( - 'updating {} -> {}.'.format( - repo_config['rev'], new_repo_config['rev'], - ), - ) - output_repos.append(new_repo_config) + if new_info.frozen: + updated_to = '{} (frozen)'.format(new_info.frozen) + else: + updated_to = new_info.rev + msg = 'updating {} -> {}.'.format(info.rev, updated_to) + output.write_line(msg) + rev_infos.append(new_info) else: output.write_line('already up to date.') - output_repos.append(repo_config) + rev_infos.append(None) if changed: - output_config = input_config.copy() - output_config['repos'] = output_repos - _write_new_config_file(config_file, output_config) + _write_new_config(config_file, rev_infos) return retv diff --git a/pre_commit/main.py b/pre_commit/main.py index fe1beafdf..8fd130f37 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -175,6 +175,10 @@ def main(argv=None): 'tagged version (the default behavior).' ), ) + autoupdate_parser.add_argument( + '--freeze', action='store_true', + help='Store "frozen" hashes in `rev` instead of tag names', + ) autoupdate_parser.add_argument( '--repo', dest='repos', action='append', metavar='REPO', help='Only update this repository -- may be specified multiple times.', @@ -313,6 +317,7 @@ def main(argv=None): return autoupdate( args.config, store, tags_only=not args.bleeding_edge, + freeze=args.freeze, repos=args.repos, ) elif args.command == 'clean': diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index ead0efe57..9a7255887 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -6,9 +6,10 @@ import pre_commit.constants as C from pre_commit import git -from pre_commit.commands.autoupdate import _update_repo +from pre_commit.commands.autoupdate import _check_hooks_still_exist_at_rev from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.autoupdate import RepositoryCannotBeUpdatedError +from pre_commit.commands.autoupdate import RevInfo from pre_commit.util import cmd_output from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import add_config_to_repo @@ -22,30 +23,114 @@ @pytest.fixture -def up_to_date_repo(tempdir_factory): +def up_to_date(tempdir_factory): yield make_repo(tempdir_factory, 'python_hooks_repo') -def test_up_to_date_repo(up_to_date_repo, store): - config = make_config_from_repo(up_to_date_repo) - input_rev = config['rev'] - ret = _update_repo(config, store, tags_only=False) - assert ret['rev'] == input_rev +@pytest.fixture +def out_of_date(tempdir_factory): + path = make_repo(tempdir_factory, 'python_hooks_repo') + original_rev = git.head_rev(path) + git_commit(cwd=path) + head_rev = git.head_rev(path) -def test_autoupdate_up_to_date_repo(up_to_date_repo, in_tmpdir, store): - # Write out the config - config = make_config_from_repo(up_to_date_repo, check=False) - write_config('.', config) + yield auto_namedtuple( + path=path, original_rev=original_rev, head_rev=head_rev, + ) - with open(C.CONFIG_FILE) as f: - before = f.read() - assert '^$' not in before - ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) - with open(C.CONFIG_FILE) as f: - after = f.read() - assert ret == 0 - assert before == after + +@pytest.fixture +def tagged(out_of_date): + cmd_output('git', 'tag', 'v1.2.3', cwd=out_of_date.path) + yield out_of_date + + +@pytest.fixture +def hook_disappearing(tempdir_factory): + path = make_repo(tempdir_factory, 'python_hooks_repo') + original_rev = git.head_rev(path) + + with modify_manifest(path) as manifest: + manifest[0]['id'] = 'bar' + + yield auto_namedtuple(path=path, original_rev=original_rev) + + +def test_rev_info_from_config(): + info = RevInfo.from_config({'repo': 'repo/path', 'rev': 'v1.2.3'}) + assert info == RevInfo('repo/path', 'v1.2.3', None) + + +def test_rev_info_update_up_to_date_repo(up_to_date): + config = make_config_from_repo(up_to_date) + info = RevInfo.from_config(config) + new_info = info.update(tags_only=False, freeze=False) + assert info == new_info + + +def test_rev_info_update_out_of_date_repo(out_of_date): + config = make_config_from_repo( + out_of_date.path, rev=out_of_date.original_rev, + ) + info = RevInfo.from_config(config) + new_info = info.update(tags_only=False, freeze=False) + assert new_info.rev == out_of_date.head_rev + + +def test_rev_info_update_non_master_default_branch(out_of_date): + # change the default branch to be not-master + cmd_output('git', '-C', out_of_date.path, 'branch', '-m', 'dev') + test_rev_info_update_out_of_date_repo(out_of_date) + + +def test_rev_info_update_tags_even_if_not_tags_only(tagged): + config = make_config_from_repo(tagged.path, rev=tagged.original_rev) + info = RevInfo.from_config(config) + new_info = info.update(tags_only=False, freeze=False) + assert new_info.rev == 'v1.2.3' + + +def test_rev_info_update_tags_only_does_not_pick_tip(tagged): + git_commit(cwd=tagged.path) + config = make_config_from_repo(tagged.path, rev=tagged.original_rev) + info = RevInfo.from_config(config) + new_info = info.update(tags_only=True, freeze=False) + assert new_info.rev == 'v1.2.3' + + +def test_rev_info_update_freeze_tag(tagged): + git_commit(cwd=tagged.path) + config = make_config_from_repo(tagged.path, rev=tagged.original_rev) + info = RevInfo.from_config(config) + new_info = info.update(tags_only=True, freeze=True) + assert new_info.rev == tagged.head_rev + assert new_info.frozen == 'v1.2.3' + + +def test_rev_info_update_does_not_freeze_if_already_sha(out_of_date): + config = make_config_from_repo( + out_of_date.path, rev=out_of_date.original_rev, + ) + info = RevInfo.from_config(config) + new_info = info.update(tags_only=True, freeze=True) + assert new_info.rev == out_of_date.head_rev + assert new_info.frozen is None + + +def test_autoupdate_up_to_date_repo(up_to_date, tmpdir, store): + contents = ( + 'repos:\n' + '- repo: {}\n' + ' rev: {}\n' + ' hooks:\n' + ' - id: foo\n' + ).format(up_to_date, git.head_rev(up_to_date)) + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write(contents) + + assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + assert cfg.read() == contents def test_autoupdate_old_revision_broken(tempdir_factory, in_tmpdir, store): @@ -68,98 +153,101 @@ def test_autoupdate_old_revision_broken(tempdir_factory, in_tmpdir, store): write_config('.', config) with open(C.CONFIG_FILE) as f: before = f.read() - ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) + assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0 with open(C.CONFIG_FILE) as f: after = f.read() - assert ret == 0 assert before != after assert update_rev in after -@pytest.fixture -def out_of_date_repo(tempdir_factory): - path = make_repo(tempdir_factory, 'python_hooks_repo') - original_rev = git.head_rev(path) - - git_commit(cwd=path) - head_rev = git.head_rev(path) - - yield auto_namedtuple( - path=path, original_rev=original_rev, head_rev=head_rev, +def test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store): + fmt = ( + 'repos:\n' + '- repo: {}\n' + ' rev: {}\n' + ' hooks:\n' + ' - id: foo\n' ) + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write(fmt.format(out_of_date.path, out_of_date.original_rev)) - -def test_out_of_date_repo(out_of_date_repo, store): - config = make_config_from_repo( - out_of_date_repo.path, rev=out_of_date_repo.original_rev, - ) - ret = _update_repo(config, store, tags_only=False) - assert ret['rev'] != out_of_date_repo.original_rev - assert ret['rev'] == out_of_date_repo.head_rev + assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + assert cfg.read() == fmt.format(out_of_date.path, out_of_date.head_rev) -def test_autoupdate_out_of_date_repo(out_of_date_repo, in_tmpdir, store): - # Write out the config - config = make_config_from_repo( - out_of_date_repo.path, rev=out_of_date_repo.original_rev, check=False, +def test_autoupdate_only_one_to_update(up_to_date, out_of_date, tmpdir, store): + fmt = ( + 'repos:\n' + '- repo: {}\n' + ' rev: {}\n' + ' hooks:\n' + ' - id: foo\n' + '- repo: {}\n' + ' rev: {}\n' + ' hooks:\n' + ' - id: foo\n' ) - write_config('.', config) + cfg = tmpdir.join(C.CONFIG_FILE) + before = fmt.format( + up_to_date, git.head_rev(up_to_date), + out_of_date.path, out_of_date.original_rev, + ) + cfg.write(before) - with open(C.CONFIG_FILE) as f: - before = f.read() - ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) - with open(C.CONFIG_FILE) as f: - after = f.read() - assert ret == 0 - assert before != after - # Make sure we don't add defaults - assert 'exclude' not in after - assert out_of_date_repo.head_rev in after + assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + assert cfg.read() == fmt.format( + up_to_date, git.head_rev(up_to_date), + out_of_date.path, out_of_date.head_rev, + ) def test_autoupdate_out_of_date_repo_with_correct_repo_name( - out_of_date_repo, in_tmpdir, store, + out_of_date, in_tmpdir, store, ): stale_config = make_config_from_repo( - out_of_date_repo.path, rev=out_of_date_repo.original_rev, check=False, + out_of_date.path, rev=out_of_date.original_rev, check=False, ) local_config = sample_local_config() config = {'repos': [stale_config, local_config]} - # Write out the config write_config('.', config) with open(C.CONFIG_FILE) as f: before = f.read() - repo_name = 'file://{}'.format(out_of_date_repo.path) - ret = autoupdate(C.CONFIG_FILE, store, tags_only=False, repos=(repo_name,)) + repo_name = 'file://{}'.format(out_of_date.path) + ret = autoupdate( + C.CONFIG_FILE, store, freeze=False, tags_only=False, + repos=(repo_name,), + ) with open(C.CONFIG_FILE) as f: after = f.read() assert ret == 0 assert before != after - assert out_of_date_repo.head_rev in after + assert out_of_date.head_rev in after assert 'local' in after def test_autoupdate_out_of_date_repo_with_wrong_repo_name( - out_of_date_repo, in_tmpdir, store, + out_of_date, in_tmpdir, store, ): - # Write out the config config = make_config_from_repo( - out_of_date_repo.path, rev=out_of_date_repo.original_rev, check=False, + out_of_date.path, rev=out_of_date.original_rev, check=False, ) write_config('.', config) with open(C.CONFIG_FILE) as f: before = f.read() # It will not update it, because the name doesn't match - ret = autoupdate(C.CONFIG_FILE, store, tags_only=False, repos=('dne',)) + ret = autoupdate( + C.CONFIG_FILE, store, freeze=False, tags_only=False, + repos=('dne',), + ) with open(C.CONFIG_FILE) as f: after = f.read() assert ret == 0 assert before == after -def test_does_not_reformat(in_tmpdir, out_of_date_repo, store): +def test_does_not_reformat(tmpdir, out_of_date, store): fmt = ( 'repos:\n' '- repo: {}\n' @@ -169,20 +257,15 @@ def test_does_not_reformat(in_tmpdir, out_of_date_repo, store): ' # These args are because reasons!\n' ' args: [foo, bar, baz]\n' ) - config = fmt.format(out_of_date_repo.path, out_of_date_repo.original_rev) - with open(C.CONFIG_FILE, 'w') as f: - f.write(config) + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write(fmt.format(out_of_date.path, out_of_date.original_rev)) - autoupdate(C.CONFIG_FILE, store, tags_only=False) - with open(C.CONFIG_FILE) as f: - after = f.read() - expected = fmt.format(out_of_date_repo.path, out_of_date_repo.head_rev) - assert after == expected + assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + expected = fmt.format(out_of_date.path, out_of_date.head_rev) + assert cfg.read() == expected -def test_loses_formatting_when_not_detectable( - out_of_date_repo, store, in_tmpdir, -): +def test_loses_formatting_when_not_detectable(out_of_date, store, tmpdir): """A best-effort attempt is made at updating rev without rewriting formatting. When the original formatting cannot be detected, this is abandoned. @@ -197,149 +280,119 @@ def test_loses_formatting_when_not_detectable( ' ],\n' ' }}\n' ']\n'.format( - pipes.quote(out_of_date_repo.path), out_of_date_repo.original_rev, + pipes.quote(out_of_date.path), out_of_date.original_rev, ) ) - with open(C.CONFIG_FILE, 'w') as f: - f.write(config) + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write(config) - autoupdate(C.CONFIG_FILE, store, tags_only=False) - with open(C.CONFIG_FILE) as f: - after = f.read() + assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 expected = ( 'repos:\n' '- repo: {}\n' ' rev: {}\n' ' hooks:\n' ' - id: foo\n' - ).format(out_of_date_repo.path, out_of_date_repo.head_rev) - assert after == expected - - -@pytest.fixture -def tagged_repo(out_of_date_repo): - cmd_output('git', 'tag', 'v1.2.3', cwd=out_of_date_repo.path) - yield out_of_date_repo + ).format(out_of_date.path, out_of_date.head_rev) + assert cfg.read() == expected -def test_autoupdate_tagged_repo(tagged_repo, in_tmpdir, store): - config = make_config_from_repo( - tagged_repo.path, rev=tagged_repo.original_rev, - ) +def test_autoupdate_tagged_repo(tagged, in_tmpdir, store): + config = make_config_from_repo(tagged.path, rev=tagged.original_rev) write_config('.', config) - ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) - assert ret == 0 + assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0 with open(C.CONFIG_FILE) as f: assert 'v1.2.3' in f.read() -@pytest.fixture -def tagged_repo_with_more_commits(tagged_repo): - git_commit(cwd=tagged_repo.path) - yield tagged_repo +def test_autoupdate_freeze(tagged, in_tmpdir, store): + config = make_config_from_repo(tagged.path, rev=tagged.original_rev) + write_config('.', config) + assert autoupdate(C.CONFIG_FILE, store, freeze=True, tags_only=False) == 0 + with open(C.CONFIG_FILE) as f: + expected = 'rev: {} # v1.2.3'.format(tagged.head_rev) + assert expected in f.read() -def test_autoupdate_tags_only(tagged_repo_with_more_commits, in_tmpdir, store): - config = make_config_from_repo( - tagged_repo_with_more_commits.path, - rev=tagged_repo_with_more_commits.original_rev, - ) + +def test_autoupdate_tags_only(tagged, in_tmpdir, store): + # add some commits after the tag + git_commit(cwd=tagged.path) + + config = make_config_from_repo(tagged.path, rev=tagged.original_rev) write_config('.', config) - ret = autoupdate(C.CONFIG_FILE, store, tags_only=True) - assert ret == 0 + assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=True) == 0 with open(C.CONFIG_FILE) as f: assert 'v1.2.3' in f.read() -def test_autoupdate_latest_no_config(out_of_date_repo, in_tmpdir, store): +def test_autoupdate_latest_no_config(out_of_date, in_tmpdir, store): config = make_config_from_repo( - out_of_date_repo.path, rev=out_of_date_repo.original_rev, + out_of_date.path, rev=out_of_date.original_rev, ) write_config('.', config) - cmd_output('git', 'rm', '-r', ':/', cwd=out_of_date_repo.path) - git_commit(cwd=out_of_date_repo.path) + cmd_output('git', 'rm', '-r', ':/', cwd=out_of_date.path) + git_commit(cwd=out_of_date.path) - ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) - assert ret == 1 + assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 1 with open(C.CONFIG_FILE) as f: - assert out_of_date_repo.original_rev in f.read() + assert out_of_date.original_rev in f.read() -@pytest.fixture -def hook_disappearing_repo(tempdir_factory): - path = make_repo(tempdir_factory, 'python_hooks_repo') - original_rev = git.head_rev(path) - - with modify_manifest(path) as manifest: - manifest[0]['id'] = 'bar' - - yield auto_namedtuple(path=path, original_rev=original_rev) - - -def test_hook_disppearing_repo_raises(hook_disappearing_repo, store): +def test_hook_disppearing_repo_raises(hook_disappearing, store): config = make_config_from_repo( - hook_disappearing_repo.path, - rev=hook_disappearing_repo.original_rev, + hook_disappearing.path, + rev=hook_disappearing.original_rev, hooks=[{'id': 'foo'}], ) + info = RevInfo.from_config(config).update(tags_only=False, freeze=False) with pytest.raises(RepositoryCannotBeUpdatedError): - _update_repo(config, store, tags_only=False) - - -def test_autoupdate_hook_disappearing_repo( - hook_disappearing_repo, in_tmpdir, store, -): - config = make_config_from_repo( - hook_disappearing_repo.path, - rev=hook_disappearing_repo.original_rev, - hooks=[{'id': 'foo'}], - check=False, - ) - write_config('.', config) + _check_hooks_still_exist_at_rev(config, info, store) - with open(C.CONFIG_FILE) as f: - before = f.read() - ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) - with open(C.CONFIG_FILE) as f: - after = f.read() - assert ret == 1 - assert before == after +def test_autoupdate_hook_disappearing_repo(hook_disappearing, tmpdir, store): + contents = ( + 'repos:\n' + '- repo: {}\n' + ' rev: {}\n' + ' hooks:\n' + ' - id: foo\n' + ).format(hook_disappearing.path, hook_disappearing.original_rev) + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write(contents) -def test_autoupdate_non_master_default_branch(up_to_date_repo, store): - # change the default branch to be not-master - cmd_output('git', '-C', up_to_date_repo, 'branch', '-m', 'dev') - test_up_to_date_repo(up_to_date_repo, store) + assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 1 + assert cfg.read() == contents def test_autoupdate_local_hooks(in_git_dir, store): config = sample_local_config() add_config_to_repo('.', config) - assert autoupdate(C.CONFIG_FILE, store, tags_only=False) == 0 + assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0 new_config_writen = read_config('.') assert len(new_config_writen['repos']) == 1 assert new_config_writen['repos'][0] == config def test_autoupdate_local_hooks_with_out_of_date_repo( - out_of_date_repo, in_tmpdir, store, + out_of_date, in_tmpdir, store, ): stale_config = make_config_from_repo( - out_of_date_repo.path, rev=out_of_date_repo.original_rev, check=False, + out_of_date.path, rev=out_of_date.original_rev, check=False, ) local_config = sample_local_config() config = {'repos': [local_config, stale_config]} write_config('.', config) - assert autoupdate(C.CONFIG_FILE, store, tags_only=False) == 0 + assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0 new_config_writen = read_config('.') assert len(new_config_writen['repos']) == 2 assert new_config_writen['repos'][0] == local_config -def test_autoupdate_meta_hooks(tmpdir, capsys, store): +def test_autoupdate_meta_hooks(tmpdir, store): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write( 'repos:\n' @@ -347,9 +400,7 @@ def test_autoupdate_meta_hooks(tmpdir, capsys, store): ' hooks:\n' ' - id: check-useless-excludes\n', ) - with tmpdir.as_cwd(): - ret = autoupdate(C.CONFIG_FILE, store, tags_only=True) - assert ret == 0 + assert autoupdate(str(cfg), store, freeze=False, tags_only=True) == 0 assert cfg.read() == ( 'repos:\n' '- repo: meta\n' @@ -368,9 +419,7 @@ def test_updates_old_format_to_new_format(tmpdir, capsys, store): ' entry: ./bin/foo.sh\n' ' language: script\n', ) - with tmpdir.as_cwd(): - ret = autoupdate(C.CONFIG_FILE, store, tags_only=True) - assert ret == 0 + assert autoupdate(str(cfg), store, freeze=False, tags_only=True) == 0 contents = cfg.read() assert contents == ( 'repos:\n' diff --git a/tests/commands/gc_test.py b/tests/commands/gc_test.py index 5be86b1b4..02b36945b 100644 --- a/tests/commands/gc_test.py +++ b/tests/commands/gc_test.py @@ -42,7 +42,7 @@ def test_gc(tempdir_factory, store, in_git_dir, cap_out): # update will clone both the old and new repo, making the old one gc-able install_hooks(C.CONFIG_FILE, store) - assert not autoupdate(C.CONFIG_FILE, store, tags_only=False) + assert not autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) assert _config_count(store) == 1 assert _repo_count(store) == 2 From 0c0427bfbdcda8dd445579bb8a2e160501ea37b5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Dec 2019 17:58:04 -0800 Subject: [PATCH 505/544] Add duration to verbose run --- pre_commit/commands/run.py | 8 ++ .../.pre-commit-hooks.yaml | 9 +-- .../resources/arbitrary_bytes_repo/hook.sh | 7 ++ .../arbitrary_bytes_repo/python3_hook.py | 13 ---- .../resources/arbitrary_bytes_repo/setup.py | 8 -- tests/commands/run_test.py | 77 ++++++++++++------- tests/commands/try_repo_test.py | 7 +- 7 files changed, 74 insertions(+), 55 deletions(-) create mode 100755 testing/resources/arbitrary_bytes_repo/hook.sh delete mode 100644 testing/resources/arbitrary_bytes_repo/python3_hook.py delete mode 100644 testing/resources/arbitrary_bytes_repo/setup.py diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 4ea55ffc5..45e603706 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -4,6 +4,7 @@ import os import re import subprocess +import time from identify.identify import tags_from_path @@ -99,6 +100,7 @@ def _run_single_hook(classifier, hook, skips, cols, verbose, use_color): cols=cols, ), ) + duration = None retcode = 0 files_modified = False out = b'' @@ -113,6 +115,7 @@ def _run_single_hook(classifier, hook, skips, cols, verbose, use_color): cols=cols, ), ) + duration = None retcode = 0 files_modified = False out = b'' @@ -123,7 +126,9 @@ def _run_single_hook(classifier, hook, skips, cols, verbose, use_color): diff_cmd = ('git', 'diff', '--no-ext-diff') diff_before = cmd_output_b(*diff_cmd, retcode=None) filenames = tuple(filenames) if hook.pass_filenames else () + time_before = time.time() retcode, out = hook.run(filenames, use_color) + duration = round(time.time() - time_before, 2) or 0 diff_after = cmd_output_b(*diff_cmd, retcode=None) # if the hook makes changes, fail the commit @@ -141,6 +146,9 @@ def _run_single_hook(classifier, hook, skips, cols, verbose, use_color): if verbose or hook.verbose or retcode or files_modified: _subtle_line('- hook id: {}'.format(hook.id), use_color) + if (verbose or hook.verbose) and duration is not None: + _subtle_line('- duration: {}s'.format(duration), use_color) + if retcode: _subtle_line('- exit code: {}'.format(retcode), use_color) diff --git a/testing/resources/arbitrary_bytes_repo/.pre-commit-hooks.yaml b/testing/resources/arbitrary_bytes_repo/.pre-commit-hooks.yaml index 2c2370092..c2aec9b9f 100644 --- a/testing/resources/arbitrary_bytes_repo/.pre-commit-hooks.yaml +++ b/testing/resources/arbitrary_bytes_repo/.pre-commit-hooks.yaml @@ -1,6 +1,5 @@ -- id: python3-hook - name: Python 3 Hook - entry: python3-hook - language: python - language_version: python3 +- id: hook + name: hook + entry: ./hook.sh + language: script files: \.py$ diff --git a/testing/resources/arbitrary_bytes_repo/hook.sh b/testing/resources/arbitrary_bytes_repo/hook.sh new file mode 100755 index 000000000..fb7dbae12 --- /dev/null +++ b/testing/resources/arbitrary_bytes_repo/hook.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Intentionally write mixed encoding to the output. This should not crash +# pre-commit and should write bytes to the output. +# '☃'.encode('UTF-8') + '²'.encode('latin1') +echo -e '\xe2\x98\x83\xb2' +# exit 1 to trigger printing +exit 1 diff --git a/testing/resources/arbitrary_bytes_repo/python3_hook.py b/testing/resources/arbitrary_bytes_repo/python3_hook.py deleted file mode 100644 index ba698a934..000000000 --- a/testing/resources/arbitrary_bytes_repo/python3_hook.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals - -import sys - - -def main(): - # Intentionally write mixed encoding to the output. This should not crash - # pre-commit and should write bytes to the output. - sys.stdout.buffer.write('☃'.encode('UTF-8') + '²'.encode('latin1') + b'\n') - # Return 1 to trigger printing - return 1 diff --git a/testing/resources/arbitrary_bytes_repo/setup.py b/testing/resources/arbitrary_bytes_repo/setup.py deleted file mode 100644 index c780e427a..000000000 --- a/testing/resources/arbitrary_bytes_repo/setup.py +++ /dev/null @@ -1,8 +0,0 @@ -from setuptools import setup - -setup( - name='python3_hook', - version='0.0.0', - py_modules=['python3_hook'], - entry_points={'console_scripts': ['python3-hook=python3_hook:main']}, -) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index e56612e3f..b7412d614 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -5,6 +5,7 @@ import os.path import pipes import sys +import time import mock import pytest @@ -25,6 +26,7 @@ from testing.fixtures import modify_config from testing.fixtures import read_config from testing.fixtures import sample_meta_config +from testing.fixtures import write_config from testing.util import cmd_output_mocked_pre_commit_home from testing.util import cwd from testing.util import git_commit @@ -163,36 +165,55 @@ def test_exclude_types_hook_repository(cap_out, store, tempdir_factory): assert b'exe' not in printed -def test_global_exclude(cap_out, store, tempdir_factory): - git_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(git_path): - with modify_config() as config: - config['exclude'] = '^foo.py$' - open('foo.py', 'a').close() - open('bar.py', 'a').close() - cmd_output('git', 'add', '.') - opts = run_opts(verbose=True) - ret, printed = _do_run(cap_out, store, git_path, opts) - assert ret == 0 - # Does not contain foo.py since it was excluded - expected = b'- hook id: bash_hook\n\nbar.py\nHello World\n\n' - assert printed.endswith(expected) +def test_global_exclude(cap_out, store, in_git_dir): + config = { + 'exclude': r'^foo\.py$', + 'repos': [{'repo': 'meta', 'hooks': [{'id': 'identity'}]}], + } + write_config('.', config) + open('foo.py', 'a').close() + open('bar.py', 'a').close() + cmd_output('git', 'add', '.') + opts = run_opts(verbose=True) + ret, printed = _do_run(cap_out, store, str(in_git_dir), opts) + assert ret == 0 + # Does not contain foo.py since it was excluded + assert printed.startswith(b'identity' + b'.' * 65 + b'Passed\n') + assert printed.endswith(b'\n\n.pre-commit-config.yaml\nbar.py\n\n') -def test_global_files(cap_out, store, tempdir_factory): - git_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(git_path): - with modify_config() as config: - config['files'] = '^bar.py$' - open('foo.py', 'a').close() - open('bar.py', 'a').close() - cmd_output('git', 'add', '.') - opts = run_opts(verbose=True) - ret, printed = _do_run(cap_out, store, git_path, opts) - assert ret == 0 - # Does not contain foo.py since it was not included - expected = b'- hook id: bash_hook\n\nbar.py\nHello World\n\n' - assert printed.endswith(expected) +def test_global_files(cap_out, store, in_git_dir): + config = { + 'files': r'^bar\.py$', + 'repos': [{'repo': 'meta', 'hooks': [{'id': 'identity'}]}], + } + write_config('.', config) + open('foo.py', 'a').close() + open('bar.py', 'a').close() + cmd_output('git', 'add', '.') + opts = run_opts(verbose=True) + ret, printed = _do_run(cap_out, store, str(in_git_dir), opts) + assert ret == 0 + # Does not contain foo.py since it was excluded + assert printed.startswith(b'identity' + b'.' * 65 + b'Passed\n') + assert printed.endswith(b'\n\nbar.py\n\n') + + +@pytest.mark.parametrize( + ('t1', 't2', 'expected'), + ( + (1.234, 2., b'\n- duration: 0.77s\n'), + (1., 1., b'\n- duration: 0s\n'), + ), +) +def test_verbose_duration(cap_out, store, in_git_dir, t1, t2, expected): + write_config('.', {'repo': 'meta', 'hooks': [{'id': 'identity'}]}) + cmd_output('git', 'add', '.') + opts = run_opts(verbose=True) + with mock.patch.object(time, 'time', side_effect=(t1, t2)): + ret, printed = _do_run(cap_out, store, str(in_git_dir), opts) + assert ret == 0 + assert expected in printed @pytest.mark.parametrize( diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index ee010636b..536eb9bc4 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -3,6 +3,9 @@ import os.path import re +import time + +import mock from pre_commit import git from pre_commit.commands.try_repo import try_repo @@ -40,7 +43,8 @@ def _run_try_repo(tempdir_factory, **kwargs): def test_try_repo_repo_only(cap_out, tempdir_factory): - _run_try_repo(tempdir_factory, verbose=True) + with mock.patch.object(time, 'time', return_value=0.0): + _run_try_repo(tempdir_factory, verbose=True) start, config, rest = _get_out(cap_out) assert start == '' assert re.match( @@ -58,6 +62,7 @@ def test_try_repo_repo_only(cap_out, tempdir_factory): - hook id: bash_hook Bash hook................................................................Passed - hook id: bash_hook2 +- duration: 0s test-file From 968b2fdaf1c4b54d6e5d6c8c857c01ecad539c74 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Dec 2019 11:00:45 -0800 Subject: [PATCH 506/544] Allow try-repo to work on bare repositories --- pre_commit/git.py | 2 +- tests/commands/try_repo_test.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index c8faf60f7..136cefef5 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -141,7 +141,7 @@ def has_diff(*args, **kwargs): repo = kwargs.pop('repo', '.') assert not kwargs, kwargs cmd = ('git', 'diff', '--quiet', '--no-ext-diff') + args - return cmd_output_b(*cmd, cwd=repo, retcode=None)[0] + return cmd_output_b(*cmd, cwd=repo, retcode=None)[0] == 1 def has_core_hookpaths_set(): diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index 536eb9bc4..1849c70a5 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -98,6 +98,15 @@ def test_try_repo_relative_path(cap_out, tempdir_factory): assert not try_repo(try_repo_opts(relative_repo, hook='bash_hook')) +def test_try_repo_bare_repo(cap_out, tempdir_factory): + repo = make_repo(tempdir_factory, 'modified_file_returns_zero_repo') + with cwd(git_dir(tempdir_factory)): + _add_test_file() + bare_repo = os.path.join(repo, '.git') + # previously crashed attempting modification changes + assert not try_repo(try_repo_opts(bare_repo, hook='bash_hook')) + + def test_try_repo_specific_revision(cap_out, tempdir_factory): repo = make_repo(tempdir_factory, 'script_hooks_repo') ref = git.head_rev(repo) From d8b54ddf4a35137f05e4ca2dd268a14708206758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yngve=20H=C3=B8iseth?= Date: Wed, 1 Jan 2020 15:27:27 +0100 Subject: [PATCH 507/544] Make URL clickable I added a space after as well in order to make it look more balanced. --- pre_commit/commands/autoupdate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index eea5be7ca..5e804c146 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -136,7 +136,7 @@ def autoupdate(config_file, store, tags_only, freeze, repos=()): rev_infos.append(None) continue - output.write('Updating {}...'.format(info.repo)) + output.write('Updating {} ... '.format(info.repo)) new_info = info.update(tags_only=tags_only, freeze=freeze) try: _check_hooks_still_exist_at_rev(repo_config, new_info, store) From 35caf115f8c0fadd1604c031825af68c34362818 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 1 Jan 2020 20:21:42 -0800 Subject: [PATCH 508/544] clear 'frozen: ...' comment if autoupdate unfreezes --- pre_commit/commands/autoupdate.py | 4 +++- tests/commands/autoupdate_test.py | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 5e804c146..05187b850 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -108,7 +108,9 @@ def _write_new_config(path, rev_infos): new_rev_s = ordered_dump({'rev': rev_info.rev}, **C.YAML_DUMP_KWARGS) new_rev = new_rev_s.split(':', 1)[1].strip() if rev_info.frozen is not None: - comment = ' # {}'.format(rev_info.frozen) + comment = ' # frozen: {}'.format(rev_info.frozen) + elif match.group(4).strip().startswith('# frozen:'): + comment = '' else: comment = match.group(4) lines[idx] = REV_LINE_FMT.format( diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 9a7255887..f8ea084e0 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -312,9 +312,14 @@ def test_autoupdate_freeze(tagged, in_tmpdir, store): assert autoupdate(C.CONFIG_FILE, store, freeze=True, tags_only=False) == 0 with open(C.CONFIG_FILE) as f: - expected = 'rev: {} # v1.2.3'.format(tagged.head_rev) + expected = 'rev: {} # frozen: v1.2.3'.format(tagged.head_rev) assert expected in f.read() + # if we un-freeze it should remove the frozen comment + assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0 + with open(C.CONFIG_FILE) as f: + assert 'rev: v1.2.3\n' in f.read() + def test_autoupdate_tags_only(tagged, in_tmpdir, store): # add some commits after the tag From 23762d39ba416515a97ca073247faeec408bee4b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 2 Jan 2020 09:46:01 -0800 Subject: [PATCH 509/544] v1.21.0 --- CHANGELOG.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 289056654..fad9b1d22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,48 @@ +1.21.0 - 2019-01-02 +=================== + +### Features +- Add `conda` as a new `language`. + - #1204 issue by @xhochy. + - #1232 PR by @xhochy. +- Add top-level configuration `files` for file selection. + - #1220 issue by @TheButlah. + - #1248 PR by @asottile. +- Rework `--verbose` / `verbose` to be more consistent with normal runs. + - #1249 PR by @asottile. +- Add support for the `pre-merge-commit` git hook. + - #1210 PR by @asottile. + - this requires git 1.24+. +- Add `pre-commit autoupdate --freeze` which produces "frozen" revisions. + - #1068 issue by @SkypLabs. + - #1256 PR by @asottile. +- Display hook runtime duration when run with `--verbose`. + - #1144 issue by @potiuk. + - #1257 PR by @asottile. + +### Fixes +- Produce better error message when erroneously running inside of `.git`. + - #1219 issue by @Nusserdt. + - #1224 PR by @asottile. + - Note: `git` has since fixed this bug: git/git@36fd304d +- Produce better error message when hook installation fails. + - #1250 issue by @asottile. + - #1251 PR by @asottile. +- Fix cloning when `GIT_SSL_CAINFO` is necessary. + - #1253 issue by @igankevich. + - #1254 PR by @igankevich. +- Fix `pre-commit try-repo` for bare, on-disk repositories. + - #1257 issue by @webknjaz. + - #1259 PR by @asottile. +- Add some whitespace to `pre-commit autoupdate` to improve terminal autolink. + - #1261 issue by @yhoiseth. + - #1262 PR by @yhoiseth. + +### Misc. +- Minor code documentation updates. + - #1200 PR by @ryanrhee. + - #1201 PR by @ryanrhee. + 1.20.0 - 2019-10-28 =================== diff --git a/setup.cfg b/setup.cfg index f9ae6e377..38b26ee8e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.20.0 +version = 1.21.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From db46dc79bb372fd5a1d1c5b695b498f2938af0fd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 2 Jan 2020 10:28:45 -0800 Subject: [PATCH 510/544] Fix one of the issue links --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fad9b1d22..af1f3fce4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,7 @@ - #1253 issue by @igankevich. - #1254 PR by @igankevich. - Fix `pre-commit try-repo` for bare, on-disk repositories. - - #1257 issue by @webknjaz. + - #1258 issue by @webknjaz. - #1259 PR by @asottile. - Add some whitespace to `pre-commit autoupdate` to improve terminal autolink. - #1261 issue by @yhoiseth. From 3fadbefab9089e84a7cc049de3c5321a659f9d1d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 2 Jan 2020 13:05:57 -0800 Subject: [PATCH 511/544] Fix git version number in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af1f3fce4..e3259277e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ - #1249 PR by @asottile. - Add support for the `pre-merge-commit` git hook. - #1210 PR by @asottile. - - this requires git 1.24+. + - this requires git 2.24+. - Add `pre-commit autoupdate --freeze` which produces "frozen" revisions. - #1068 issue by @SkypLabs. - #1256 PR by @asottile. From 97e33710466bf444be56454915130e8e0a0458d8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 5 Jan 2020 13:58:44 -0800 Subject: [PATCH 512/544] Remove deprecated `pcre` language --- pre_commit/commands/run.py | 8 ---- pre_commit/languages/all.py | 2 - pre_commit/languages/pcre.py | 22 ----------- pre_commit/repository.py | 2 +- pre_commit/xargs.py | 4 -- testing/util.py | 11 ------ tests/commands/run_test.py | 26 ------------- tests/repository_test.py | 73 +++++++++++------------------------- tests/xargs_test.py | 17 --------- 9 files changed, 22 insertions(+), 143 deletions(-) delete mode 100644 pre_commit/languages/pcre.py diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 45e603706..c8baed886 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -82,14 +82,6 @@ def _subtle_line(s, use_color): def _run_single_hook(classifier, hook, skips, cols, verbose, use_color): filenames = classifier.filenames_for_hook(hook) - if hook.language == 'pcre': - logger.warning( - '`{}` (from {}) uses the deprecated pcre language.\n' - 'The pcre language is scheduled for removal in pre-commit 2.x.\n' - 'The pygrep language is a more portable (and usually drop-in) ' - 'replacement.'.format(hook.id, hook.src), - ) - if hook.id in skips or hook.alias in skips: output.write( get_hook_message( diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 3d139d984..c14877861 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -6,7 +6,6 @@ from pre_commit.languages import fail from pre_commit.languages import golang from pre_commit.languages import node -from pre_commit.languages import pcre from pre_commit.languages import pygrep from pre_commit.languages import python from pre_commit.languages import python_venv @@ -59,7 +58,6 @@ 'fail': fail, 'golang': golang, 'node': node, - 'pcre': pcre, 'pygrep': pygrep, 'python': python, 'python_venv': python_venv, diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py deleted file mode 100644 index 2d8bdfa01..000000000 --- a/pre_commit/languages/pcre.py +++ /dev/null @@ -1,22 +0,0 @@ -from __future__ import unicode_literals - -import sys - -from pre_commit.languages import helpers -from pre_commit.xargs import xargs - - -ENVIRONMENT_DIR = None -GREP = 'ggrep' if sys.platform == 'darwin' else 'grep' -get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy -install_environment = helpers.no_install - - -def run_hook(hook, file_args, color): - # For PCRE the entry is the regular expression to match - cmd = (GREP, '-H', '-n', '-P') + tuple(hook.args) + (hook.entry,) - - # Grep usually returns 0 for matches, and nonzero for non-matches so we - # negate it here. - return xargs(cmd, file_args, negate=True, color=color) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 3042f12dc..829fe47ca 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -149,7 +149,7 @@ def _hook(*hook_dicts, **kwargs): def _non_cloned_repository_hooks(repo_config, store, root_config): def _prefix(language_name, deps): language = languages[language_name] - # pcre / pygrep / script / system / docker_image do not have + # pygrep / script / system / docker_image do not have # environments so they work out of the current directory if language.ENVIRONMENT_DIR is None: return Prefix(os.getcwd()) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 5e405903b..ace82f5a3 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -107,11 +107,9 @@ def xargs(cmd, varargs, **kwargs): """A simplified implementation of xargs. color: Make a pty if on a platform that supports it - negate: Make nonzero successful and zero a failure target_concurrency: Target number of partitions to run concurrently """ color = kwargs.pop('color', False) - negate = kwargs.pop('negate', False) target_concurrency = kwargs.pop('target_concurrency', 1) max_length = kwargs.pop('_max_length', _get_platform_max_length()) cmd_fn = cmd_output_p if color else cmd_output_b @@ -135,8 +133,6 @@ def run_cmd_partition(run_cmd): results = thread_map(run_cmd_partition, partitions) for proc_retcode, proc_out, _ in results: - if negate: - proc_retcode = not proc_retcode retcode = max(retcode, proc_retcode) stdout += proc_out diff --git a/testing/util.py b/testing/util.py index 600f1c593..a2a2e24f3 100644 --- a/testing/util.py +++ b/testing/util.py @@ -9,7 +9,6 @@ from pre_commit import parse_shebang from pre_commit.languages.docker import docker_is_running -from pre_commit.languages.pcre import GREP from pre_commit.util import cmd_output from testing.auto_namedtuple import auto_namedtuple @@ -68,16 +67,6 @@ def broken_deep_listdir(): # pragma: no cover (platform specific) ) -def platform_supports_pcre(): - output = cmd_output(GREP, '-P', "Don't", 'CHANGELOG.md', retcode=None) - return output[0] == 0 and "Don't use readlink -f" in output[1] - - -xfailif_no_pcre_support = pytest.mark.xfail( - not platform_supports_pcre(), - reason='grep -P is not supported on this platform', -) - xfailif_no_symlink = pytest.mark.xfail( not hasattr(os, 'symlink'), reason='Symlink is not supported on this platform', diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index b7412d614..58d40fe3b 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -734,32 +734,6 @@ def test_local_hook_fails(cap_out, store, repo_with_passing_hook): ) -def test_pcre_deprecation_warning(cap_out, store, repo_with_passing_hook): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'pcre-hook', - 'name': 'pcre-hook', - 'language': 'pcre', - 'entry': '.', - }], - } - add_config_to_repo(repo_with_passing_hook, config) - - _test_run( - cap_out, - store, - repo_with_passing_hook, - opts={}, - expected_outputs=[ - b'[WARNING] `pcre-hook` (from local) uses the deprecated ' - b'pcre language.', - ], - expected_ret=0, - stage=False, - ) - - def test_meta_hook_passes(cap_out, store, repo_with_passing_hook): add_config_to_repo(repo_with_passing_hook, sample_meta_config()) diff --git a/tests/repository_test.py b/tests/repository_test.py index a468e707c..1f06b355a 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -12,14 +12,12 @@ import pre_commit.constants as C from pre_commit import five -from pre_commit import parse_shebang from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest from pre_commit.envcontext import envcontext from pre_commit.languages import golang from pre_commit.languages import helpers from pre_commit.languages import node -from pre_commit.languages import pcre from pre_commit.languages import python from pre_commit.languages import ruby from pre_commit.languages import rust @@ -37,7 +35,6 @@ from testing.util import skipif_cant_run_docker from testing.util import skipif_cant_run_swift from testing.util import xfailif_broken_deep_listdir -from testing.util import xfailif_no_pcre_support from testing.util import xfailif_no_venv from testing.util import xfailif_windows_no_ruby @@ -426,13 +423,13 @@ def test_output_isatty(tempdir_factory, store): ) -def _make_grep_repo(language, entry, store, args=()): +def _make_grep_repo(entry, store, args=()): config = { 'repo': 'local', 'hooks': [{ 'id': 'grep-hook', 'name': 'grep-hook', - 'language': language, + 'language': 'pygrep', 'entry': entry, 'args': args, 'types': ['text'], @@ -451,53 +448,25 @@ def greppable_files(tmpdir): yield tmpdir -class TestPygrep(object): - language = 'pygrep' - - def test_grep_hook_matching(self, greppable_files, store): - hook = _make_grep_repo(self.language, 'ello', store) - ret, out = hook.run(('f1', 'f2', 'f3'), color=False) - assert ret == 1 - assert _norm_out(out) == b"f1:1:hello'hi\n" - - def test_grep_hook_case_insensitive(self, greppable_files, store): - hook = _make_grep_repo(self.language, 'ELLO', store, args=['-i']) - ret, out = hook.run(('f1', 'f2', 'f3'), color=False) - assert ret == 1 - assert _norm_out(out) == b"f1:1:hello'hi\n" - - @pytest.mark.parametrize('regex', ('nope', "foo'bar", r'^\[INFO\]')) - def test_grep_hook_not_matching(self, regex, greppable_files, store): - hook = _make_grep_repo(self.language, regex, store) - ret, out = hook.run(('f1', 'f2', 'f3'), color=False) - assert (ret, out) == (0, b'') - - -@xfailif_no_pcre_support # pragma: windows no cover -class TestPCRE(TestPygrep): - """organized as a class for xfailing pcre""" - language = 'pcre' - - def test_pcre_hook_many_files(self, greppable_files, store): - # This is intended to simulate lots of passing files and one failing - # file to make sure it still fails. This is not the case when naively - # using a system hook with `grep -H -n '...'` - hook = _make_grep_repo('pcre', 'ello', store) - ret, out = hook.run((os.devnull,) * 15000 + ('f1',), color=False) - assert ret == 1 - assert _norm_out(out) == b"f1:1:hello'hi\n" - - def test_missing_pcre_support(self, greppable_files, store): - def no_grep(exe, **kwargs): - assert exe == pcre.GREP - return None - - with mock.patch.object(parse_shebang, 'find_executable', no_grep): - hook = _make_grep_repo('pcre', 'ello', store) - ret, out = hook.run(('f1', 'f2', 'f3'), color=False) - assert ret == 1 - expected = 'Executable `{}` not found'.format(pcre.GREP).encode() - assert out == expected +def test_grep_hook_matching(greppable_files, store): + hook = _make_grep_repo('ello', store) + ret, out = hook.run(('f1', 'f2', 'f3'), color=False) + assert ret == 1 + assert _norm_out(out) == b"f1:1:hello'hi\n" + + +def test_grep_hook_case_insensitive(greppable_files, store): + hook = _make_grep_repo('ELLO', store, args=['-i']) + ret, out = hook.run(('f1', 'f2', 'f3'), color=False) + assert ret == 1 + assert _norm_out(out) == b"f1:1:hello'hi\n" + + +@pytest.mark.parametrize('regex', ('nope', "foo'bar", r'^\[INFO\]')) +def test_grep_hook_not_matching(regex, greppable_files, store): + hook = _make_grep_repo(regex, store) + ret, out = hook.run(('f1', 'f2', 'f3'), color=False) + assert (ret, out) == (0, b'') def _norm_pwd(path): diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 65b1d495b..49bf70f60 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -154,23 +154,6 @@ def test_xargs_smoke(): max_length = len(' '.join(exit_cmd)) + 3 -def test_xargs_negate(): - ret, _ = xargs.xargs( - exit_cmd, ('1',), negate=True, _max_length=max_length, - ) - assert ret == 0 - - ret, _ = xargs.xargs( - exit_cmd, ('1', '0'), negate=True, _max_length=max_length, - ) - assert ret == 1 - - -def test_xargs_negate_command_not_found(): - ret, _ = xargs.xargs(('cmd-not-found',), ('1',), negate=True) - assert ret != 0 - - def test_xargs_retcode_normal(): ret, _ = xargs.xargs(exit_cmd, ('0',), _max_length=max_length) assert ret == 0 From ae97bb50681147be680477234fdabe718270fa74 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 5 Jan 2020 14:04:41 -0800 Subject: [PATCH 513/544] Remove autoupdate --tags-only option --- pre_commit/main.py | 5 ----- tests/main_test.py | 5 ----- 2 files changed, 10 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 8fd130f37..654e8f843 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -165,9 +165,6 @@ def main(argv=None): ) _add_color_option(autoupdate_parser) _add_config_option(autoupdate_parser) - autoupdate_parser.add_argument( - '--tags-only', action='store_true', help='LEGACY: for compatibility', - ) autoupdate_parser.add_argument( '--bleeding-edge', action='store_true', help=( @@ -312,8 +309,6 @@ def main(argv=None): store.mark_config_used(args.config) if args.command == 'autoupdate': - if args.tags_only: - logger.warning('--tags-only is the default') return autoupdate( args.config, store, tags_only=not args.bleeding_edge, diff --git a/tests/main_test.py b/tests/main_test.py index b59d35ef1..c2c7a8657 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -190,8 +190,3 @@ def test_expected_fatal_error_no_git_repo(in_tmpdir, cap_out, mock_store_dir): 'Is it installed, and are you in a Git repository directory?' ) assert cap_out_lines[-1] == 'Check the log at {}'.format(log_file) - - -def test_warning_on_tags_only(mock_commands, cap_out, mock_store_dir): - main.main(('autoupdate', '--tags-only')) - assert '--tags-only is the default' in cap_out.get() From 8f109890c2327a48d382c177c319838258b43bdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Flaud=C3=ADsio=20Tolentino?= Date: Thu, 9 Jan 2020 17:20:16 -0300 Subject: [PATCH 514/544] Fix the v1.21.0 release date in Changelog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Flaudísio Tolentino --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3259277e..18322ad01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -1.21.0 - 2019-01-02 +1.21.0 - 2020-01-02 =================== ### Features From 2cf127f2d3dff574bc504eaecf9cb4e06d0f156e Mon Sep 17 00:00:00 2001 From: orcutt989 Date: Fri, 10 Jan 2020 18:43:13 -0500 Subject: [PATCH 515/544] fix prog arg to return correct version --- pre_commit/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 654e8f843..423339b89 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -148,7 +148,7 @@ def _adjust_args_and_chdir(args): def main(argv=None): argv = argv if argv is not None else sys.argv[1:] argv = [five.to_text(arg) for arg in argv] - parser = argparse.ArgumentParser() + parser = argparse.ArgumentParser(prog='pre_commit') # https://stackoverflow.com/a/8521644/812183 parser.add_argument( From c7d938c2c45d457d4c6c3bcc91c13b1d4154c3ab Mon Sep 17 00:00:00 2001 From: orcutt989 Date: Fri, 10 Jan 2020 18:49:21 -0500 Subject: [PATCH 516/544] corrected styling --- pre_commit/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 423339b89..8ae145a8b 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -148,7 +148,7 @@ def _adjust_args_and_chdir(args): def main(argv=None): argv = argv if argv is not None else sys.argv[1:] argv = [five.to_text(arg) for arg in argv] - parser = argparse.ArgumentParser(prog='pre_commit') + parser = argparse.ArgumentParser(prog='pre-commit') # https://stackoverflow.com/a/8521644/812183 parser.add_argument( From 30c1e8289f062d73d904bff3e4f3b067b6a1a8b2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 8 Jan 2020 20:49:09 -0800 Subject: [PATCH 517/544] upgrade hooks, pyupgrade pre-commit --- .pre-commit-config.yaml | 22 ++++++++----- azure-pipelines.yml | 7 ++--- pre_commit/__main__.py | 2 -- pre_commit/clientlib.py | 11 +++---- pre_commit/color.py | 6 ++-- pre_commit/color_windows.py | 3 -- pre_commit/commands/autoupdate.py | 14 +++------ pre_commit/commands/clean.py | 5 +-- pre_commit/commands/gc.py | 5 +-- pre_commit/commands/init_templatedir.py | 2 +- pre_commit/commands/install_uninstall.py | 18 +++++------ pre_commit/commands/migrate_config.py | 8 ++--- pre_commit/commands/run.py | 10 +++--- pre_commit/commands/sample_config.py | 5 --- pre_commit/commands/try_repo.py | 3 -- pre_commit/constants.py | 3 -- pre_commit/envcontext.py | 3 -- pre_commit/error_handler.py | 20 +++++------- pre_commit/file_lock.py | 9 ++---- pre_commit/five.py | 5 +-- pre_commit/git.py | 4 +-- pre_commit/languages/all.py | 2 -- pre_commit/languages/conda.py | 2 +- pre_commit/languages/docker.py | 5 +-- pre_commit/languages/docker_image.py | 3 -- pre_commit/languages/fail.py | 2 -- pre_commit/languages/golang.py | 2 -- pre_commit/languages/helpers.py | 13 ++------ pre_commit/languages/node.py | 2 -- pre_commit/languages/pygrep.py | 5 +-- pre_commit/languages/python.py | 2 -- pre_commit/languages/python_venv.py | 8 +---- pre_commit/languages/ruby.py | 7 ++--- pre_commit/languages/rust.py | 4 +-- pre_commit/languages/script.py | 2 -- pre_commit/languages/swift.py | 2 -- pre_commit/languages/system.py | 2 -- pre_commit/logging_handler.py | 6 ++-- pre_commit/main.py | 12 +++---- pre_commit/make_archives.py | 6 +--- pre_commit/meta_hooks/check_hooks_apply.py | 2 +- .../meta_hooks/check_useless_excludes.py | 2 -- pre_commit/output.py | 2 -- pre_commit/parse_shebang.py | 5 +-- pre_commit/prefix.py | 2 -- pre_commit/repository.py | 9 ++---- pre_commit/resources/hook-tmpl | 8 ++--- pre_commit/staged_files_only.py | 9 ++---- pre_commit/store.py | 13 +++----- pre_commit/util.py | 16 +++------- pre_commit/xargs.py | 15 ++------- setup.cfg | 7 ++--- testing/auto_namedtuple.py | 2 -- testing/fixtures.py | 18 +++++------ .../resources/python3_hooks_repo/py3_hook.py | 2 -- testing/resources/python_hooks_repo/foo.py | 2 -- .../resources/python_venv_hooks_repo/foo.py | 2 -- .../stdout_stderr_repo/stdout-stderr-entry | 2 +- testing/util.py | 4 +-- tests/clientlib_test.py | 2 -- tests/color_test.py | 4 +-- tests/commands/autoupdate_test.py | 6 ++-- tests/commands/clean_test.py | 2 -- tests/commands/install_uninstall_test.py | 27 +++++++--------- tests/commands/migrate_config_test.py | 3 -- tests/commands/run_test.py | 20 +++++------- tests/commands/sample_config_test.py | 3 -- tests/commands/try_repo_test.py | 3 -- tests/conftest.py | 26 +++++++--------- tests/envcontext_test.py | 3 -- tests/error_handler_test.py | 9 ++---- tests/git_test.py | 4 --- tests/languages/all_test.py | 19 +++--------- tests/languages/docker_test.py | 3 -- tests/languages/golang_test.py | 3 -- tests/languages/helpers_test.py | 3 -- tests/languages/pygrep_test.py | 3 -- tests/languages/python_test.py | 5 +-- tests/languages/ruby_test.py | 2 -- tests/logging_handler_test.py | 4 +-- tests/main_test.py | 7 ++--- tests/make_archives_test.py | 5 +-- tests/output_test.py | 2 -- tests/parse_shebang_test.py | 10 ++---- tests/prefix_test.py | 2 -- tests/repository_test.py | 7 ++--- tests/staged_files_only_test.py | 31 ++++++++----------- tests/store_test.py | 9 ++---- tests/util_test.py | 6 ++-- tests/xargs_test.py | 4 --- tox.ini | 2 +- 91 files changed, 176 insertions(+), 437 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1b87a4068..aa540e828 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.1.0 + rev: v2.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -12,30 +12,36 @@ repos: - id: requirements-txt-fixer - id: double-quote-string-fixer - repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.7 + rev: 3.7.9 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.4.3 + rev: v1.4.4 hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v1.14.4 + rev: v1.21.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v1.12.0 + rev: v1.25.3 hooks: - id: pyupgrade + args: [--py36-plus] - repo: https://github.com/asottile/reorder_python_imports - rev: v1.4.0 + rev: v1.9.0 hooks: - id: reorder-python-imports - language_version: python3 + args: [--py3-plus] - repo: https://github.com/asottile/add-trailing-comma - rev: v1.0.0 + rev: v1.5.0 hooks: - id: add-trailing-comma + args: [--py36-plus] +- repo: https://github.com/asottile/setup-cfg-fmt + rev: v1.6.0 + hooks: + - id: setup-cfg-fmt - repo: meta hooks: - id: check-hooks-apply diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 9d61eb648..b9f0b5f3b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -10,18 +10,17 @@ resources: type: github endpoint: github name: asottile/azure-pipeline-templates - ref: refs/tags/v0.0.15 + ref: refs/tags/v1.0.0 jobs: - template: job--pre-commit.yml@asottile - template: job--python-tox.yml@asottile parameters: - toxenvs: [py27, py37] + toxenvs: [py37] os: windows additional_variables: COVERAGE_IGNORE_WINDOWS: '# pragma: windows no cover' TOX_TESTENV_PASSENV: COVERAGE_IGNORE_WINDOWS - TEMP: C:\Temp # remove when dropping python2 pre_test: - powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts" displayName: Add conda to PATH @@ -39,7 +38,7 @@ jobs: displayName: install swift - template: job--python-tox.yml@asottile parameters: - toxenvs: [pypy, pypy3, py27, py36, py37, py38] + toxenvs: [pypy3, py36, py37, py38] os: linux pre_test: - task: UseRubyVersion@0 diff --git a/pre_commit/__main__.py b/pre_commit/__main__.py index fc424d821..541406879 100644 --- a/pre_commit/__main__.py +++ b/pre_commit/__main__.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from pre_commit.main import main diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 74a37a8f3..c02de282d 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import argparse import functools import logging @@ -106,7 +103,7 @@ def validate_manifest_main(argv=None): META = 'meta' -class MigrateShaToRev(object): +class MigrateShaToRev: key = 'rev' @staticmethod @@ -202,7 +199,7 @@ def warn_unknown_keys_repo(extra, orig_keys, dct): if item.key in {'name', 'language', 'entry'} else item for item in MANIFEST_HOOK_DICT.items - ]) + ]), ) CONFIG_HOOK_DICT = cfgv.Map( 'Hook', 'id', @@ -217,7 +214,7 @@ def warn_unknown_keys_repo(extra, orig_keys, dct): cfgv.OptionalNoDefault(item.key, item.check_fn) for item in MANIFEST_HOOK_DICT.items if item.key != 'id' - ] + ], ) CONFIG_REPO_DICT = cfgv.Map( 'Repository', 'repo', @@ -243,7 +240,7 @@ def warn_unknown_keys_repo(extra, orig_keys, dct): DEFAULT_LANGUAGE_VERSION = cfgv.Map( 'DefaultLanguageVersion', None, cfgv.NoAdditionalKeys(all_languages), - *[cfgv.Optional(x, cfgv.check_string, C.DEFAULT) for x in all_languages] + *[cfgv.Optional(x, cfgv.check_string, C.DEFAULT) for x in all_languages], ) CONFIG_SCHEMA = cfgv.Map( 'Config', None, diff --git a/pre_commit/color.py b/pre_commit/color.py index 7a138f47f..667609b40 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import os import sys @@ -8,7 +6,7 @@ from pre_commit.color_windows import enable_virtual_terminal_processing try: enable_virtual_terminal_processing() - except WindowsError: + except OSError: terminal_supports_color = False RED = '\033[41m' @@ -34,7 +32,7 @@ def format_color(text, color, use_color_setting): if not use_color_setting: return text else: - return '{}{}{}'.format(color, text, NORMAL) + return f'{color}{text}{NORMAL}' COLOR_CHOICES = ('auto', 'always', 'never') diff --git a/pre_commit/color_windows.py b/pre_commit/color_windows.py index 9b8555e8d..3e6e3ca9e 100644 --- a/pre_commit/color_windows.py +++ b/pre_commit/color_windows.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - from ctypes import POINTER from ctypes import windll from ctypes import WinError diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 05187b850..12e67dce0 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -1,11 +1,7 @@ -from __future__ import print_function -from __future__ import unicode_literals - import collections import os.path import re -import six from aspy.yaml import ordered_dump from aspy.yaml import ordered_load @@ -64,7 +60,7 @@ def _check_hooks_still_exist_at_rev(repo_config, info, store): path = store.clone(repo_config['repo'], info.rev) manifest = load_manifest(os.path.join(path, C.MANIFEST_FILE)) except InvalidManifestError as e: - raise RepositoryCannotBeUpdatedError(six.text_type(e)) + raise RepositoryCannotBeUpdatedError(str(e)) # See if any of our hooks were deleted with the new commits hooks = {hook['id'] for hook in repo_config['hooks']} @@ -108,7 +104,7 @@ def _write_new_config(path, rev_infos): new_rev_s = ordered_dump({'rev': rev_info.rev}, **C.YAML_DUMP_KWARGS) new_rev = new_rev_s.split(':', 1)[1].strip() if rev_info.frozen is not None: - comment = ' # frozen: {}'.format(rev_info.frozen) + comment = f' # frozen: {rev_info.frozen}' elif match.group(4).strip().startswith('# frozen:'): comment = '' else: @@ -138,7 +134,7 @@ def autoupdate(config_file, store, tags_only, freeze, repos=()): rev_infos.append(None) continue - output.write('Updating {} ... '.format(info.repo)) + output.write(f'Updating {info.repo} ... ') new_info = info.update(tags_only=tags_only, freeze=freeze) try: _check_hooks_still_exist_at_rev(repo_config, new_info, store) @@ -151,10 +147,10 @@ def autoupdate(config_file, store, tags_only, freeze, repos=()): if new_info.rev != info.rev: changed = True if new_info.frozen: - updated_to = '{} (frozen)'.format(new_info.frozen) + updated_to = f'{new_info.frozen} (frozen)' else: updated_to = new_info.rev - msg = 'updating {} -> {}.'.format(info.rev, updated_to) + msg = f'updating {info.rev} -> {updated_to}.' output.write_line(msg) rev_infos.append(new_info) else: diff --git a/pre_commit/commands/clean.py b/pre_commit/commands/clean.py index 5c7630292..fe9b40784 100644 --- a/pre_commit/commands/clean.py +++ b/pre_commit/commands/clean.py @@ -1,6 +1,3 @@ -from __future__ import print_function -from __future__ import unicode_literals - import os.path from pre_commit import output @@ -12,5 +9,5 @@ def clean(store): for directory in (store.directory, legacy_path): if os.path.exists(directory): rmtree(directory) - output.write_line('Cleaned {}.'.format(directory)) + output.write_line(f'Cleaned {directory}.') return 0 diff --git a/pre_commit/commands/gc.py b/pre_commit/commands/gc.py index 65818e50e..d35a2c90a 100644 --- a/pre_commit/commands/gc.py +++ b/pre_commit/commands/gc.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import os.path import pre_commit.constants as C @@ -79,5 +76,5 @@ def _gc_repos(store): def gc(store): with store.exclusive_lock(): repos_removed = _gc_repos(store) - output.write_line('{} repo(s) removed.'.format(repos_removed)) + output.write_line(f'{repos_removed} repo(s) removed.') return 0 diff --git a/pre_commit/commands/init_templatedir.py b/pre_commit/commands/init_templatedir.py index 74a32f2b6..05c902e8e 100644 --- a/pre_commit/commands/init_templatedir.py +++ b/pre_commit/commands/init_templatedir.py @@ -23,5 +23,5 @@ def init_templatedir(config_file, store, directory, hook_types): if configured_path != dest: logger.warning('`init.templateDir` not set to the target directory') logger.warning( - 'maybe `git config --global init.templateDir {}`?'.format(dest), + f'maybe `git config --global init.templateDir {dest}`?', ) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index d6d7ac934..6d3a32243 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -1,7 +1,3 @@ -from __future__ import print_function -from __future__ import unicode_literals - -import io import itertools import logging import os.path @@ -36,13 +32,13 @@ def _hook_paths(hook_type, git_dir=None): git_dir = git_dir if git_dir is not None else git.get_git_dir() pth = os.path.join(git_dir, 'hooks', hook_type) - return pth, '{}.legacy'.format(pth) + return pth, f'{pth}.legacy' def is_our_script(filename): if not os.path.exists(filename): # pragma: windows no cover (symlink) return False - with io.open(filename) as f: + with open(filename) as f: contents = f.read() return any(h in contents for h in (CURRENT_HASH,) + PRIOR_HASHES) @@ -63,7 +59,7 @@ def shebang(): break else: py = 'python' - return '#!/usr/bin/env {}'.format(py) + return f'#!/usr/bin/env {py}' def _install_hook_script( @@ -94,7 +90,7 @@ def _install_hook_script( 'SKIP_ON_MISSING_CONFIG': skip_on_missing_config, } - with io.open(hook_path, 'w') as hook_file: + with open(hook_path, 'w') as hook_file: contents = resource_text('hook-tmpl') before, rest = contents.split(TEMPLATE_START) to_template, after = rest.split(TEMPLATE_END) @@ -108,7 +104,7 @@ def _install_hook_script( hook_file.write(TEMPLATE_END + after) make_executable(hook_path) - output.write_line('pre-commit installed at {}'.format(hook_path)) + output.write_line(f'pre-commit installed at {hook_path}') def install( @@ -149,11 +145,11 @@ def _uninstall_hook_script(hook_type): # type: (str) -> None return os.remove(hook_path) - output.write_line('{} uninstalled'.format(hook_type)) + output.write_line(f'{hook_type} uninstalled') if os.path.exists(legacy_path): os.rename(legacy_path, hook_path) - output.write_line('Restored previous hooks to {}'.format(hook_path)) + output.write_line(f'Restored previous hooks to {hook_path}') def uninstall(hook_types): diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index bac423193..7ea7a6eda 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -1,7 +1,3 @@ -from __future__ import print_function -from __future__ import unicode_literals - -import io import re import yaml @@ -47,14 +43,14 @@ def _migrate_sha_to_rev(contents): def migrate_config(config_file, quiet=False): - with io.open(config_file) as f: + with open(config_file) as f: orig_contents = contents = f.read() contents = _migrate_map(contents) contents = _migrate_sha_to_rev(contents) if contents != orig_contents: - with io.open(config_file, 'w') as f: + with open(config_file, 'w') as f: f.write(contents) print('Configuration has been migrated.') diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index c8baed886..f56fa9035 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import logging import os import re @@ -32,7 +30,7 @@ def filter_by_include_exclude(names, include, exclude): ] -class Classifier(object): +class Classifier: def __init__(self, filenames): # on windows we normalize all filenames to use forward slashes # this makes it easier to filter using the `files:` regex @@ -136,13 +134,13 @@ def _run_single_hook(classifier, hook, skips, cols, verbose, use_color): output.write_line(color.format_color(status, print_color, use_color)) if verbose or hook.verbose or retcode or files_modified: - _subtle_line('- hook id: {}'.format(hook.id), use_color) + _subtle_line(f'- hook id: {hook.id}', use_color) if (verbose or hook.verbose) and duration is not None: - _subtle_line('- duration: {}s'.format(duration), use_color) + _subtle_line(f'- duration: {duration}s', use_color) if retcode: - _subtle_line('- exit code: {}'.format(retcode), use_color) + _subtle_line(f'- exit code: {retcode}', use_color) # Print a message if failing due to file modifications if files_modified: diff --git a/pre_commit/commands/sample_config.py b/pre_commit/commands/sample_config.py index a35ef8e5c..60da7cfae 100644 --- a/pre_commit/commands/sample_config.py +++ b/pre_commit/commands/sample_config.py @@ -1,8 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - - # TODO: maybe `git ls-remote git://github.com/pre-commit/pre-commit-hooks` to # determine the latest revision? This adds ~200ms from my tests (and is # significantly faster than https:// or http://). For now, periodically diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index b7b0c990b..061120639 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import collections import logging import os.path diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 3aa452c40..aad7c498f 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import sys if sys.version_info < (3, 8): # pragma: no cover ( None @@ -44,13 +38,13 @@ def _log_line(*s): # type: (*str) -> None _log_line('### version information') _log_line() _log_line('```') - _log_line('pre-commit version: {}'.format(C.VERSION)) + _log_line(f'pre-commit version: {C.VERSION}') _log_line('sys.version:') for line in sys.version.splitlines(): - _log_line(' {}'.format(line)) - _log_line('sys.executable: {}'.format(sys.executable)) - _log_line('os.name: {}'.format(os.name)) - _log_line('sys.platform: {}'.format(sys.platform)) + _log_line(f' {line}') + _log_line(f'sys.executable: {sys.executable}') + _log_line(f'os.name: {os.name}') + _log_line(f'sys.platform: {sys.platform}') _log_line('```') _log_line() diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index cf9aeac5a..cd7ad043e 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import contextlib import errno @@ -18,12 +15,12 @@ def _locked(fileno, blocked_cb): try: msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region) - except IOError: + except OSError: blocked_cb() while True: try: msvcrt.locking(fileno, msvcrt.LK_LOCK, _region) - except IOError as e: + except OSError as e: # Locking violation. Returned when the _LK_LOCK or _LK_RLCK # flag is specified and the file cannot be locked after 10 # attempts. @@ -48,7 +45,7 @@ def _locked(fileno, blocked_cb): def _locked(fileno, blocked_cb): try: fcntl.flock(fileno, fcntl.LOCK_EX | fcntl.LOCK_NB) - except IOError: # pragma: no cover (tests are single-threaded) + except OSError: # pragma: no cover (tests are single-threaded) blocked_cb() fcntl.flock(fileno, fcntl.LOCK_EX) try: diff --git a/pre_commit/five.py b/pre_commit/five.py index 3b94a927a..8d9e5767d 100644 --- a/pre_commit/five.py +++ b/pre_commit/five.py @@ -1,11 +1,8 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import six def to_text(s): - return s if isinstance(s, six.text_type) else s.decode('UTF-8') + return s if isinstance(s, str) else s.decode('UTF-8') def to_bytes(s): diff --git a/pre_commit/git.py b/pre_commit/git.py index 136cefef5..4ced8e83f 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import logging import os.path import sys @@ -127,7 +125,7 @@ def get_changed_files(new, old): return zsplit( cmd_output( 'git', 'diff', '--name-only', '--no-ext-diff', '-z', - '{}...{}'.format(old, new), + f'{old}...{new}', )[1], ) diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index c14877861..bf7bb295f 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from pre_commit.languages import conda from pre_commit.languages import docker from pre_commit.languages import docker_image diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index a89d6c92b..fe391c051 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -53,7 +53,7 @@ def install_environment(prefix, version, additional_dependencies): if additional_dependencies: cmd_output_b( 'conda', 'install', '-p', env_dir, *additional_dependencies, - cwd=prefix.prefix_dir + cwd=prefix.prefix_dir, ) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 66f5a7c98..eae9eec97 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import hashlib import os @@ -24,7 +21,7 @@ def md5(s): # pragma: windows no cover def docker_tag(prefix): # pragma: windows no cover md5sum = md5(os.path.basename(prefix.prefix_dir)).lower() - return 'pre-commit-{}'.format(md5sum) + return f'pre-commit-{md5sum}' def docker_is_running(): # pragma: windows no cover diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index 7bd5c3140..802354011 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - from pre_commit.languages import helpers from pre_commit.languages.docker import assert_docker_available from pre_commit.languages.docker import docker_cmd diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index 4bac1f869..641cbbea4 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from pre_commit.languages import helpers diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index d85a55c67..4f121f248 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import contextlib import os.path import sys diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index dab7373c0..134a35d05 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -1,11 +1,7 @@ -from __future__ import unicode_literals - import multiprocessing import os import random -import six - import pre_commit.constants as C from pre_commit.util import cmd_output_b from pre_commit.xargs import xargs @@ -21,13 +17,13 @@ def environment_dir(ENVIRONMENT_DIR, language_version): if ENVIRONMENT_DIR is None: return None else: - return '{}-{}'.format(ENVIRONMENT_DIR, language_version) + return f'{ENVIRONMENT_DIR}-{language_version}' def assert_version_default(binary, version): if version != C.DEFAULT: raise AssertionError( - 'For now, pre-commit requires system-installed {}'.format(binary), + f'For now, pre-commit requires system-installed {binary}', ) @@ -68,10 +64,7 @@ def target_concurrency(hook): def _shuffled(seq): """Deterministically shuffle identically under both py2 + py3.""" fixed_random = random.Random() - if six.PY2: # pragma: no cover (py2) - fixed_random.seed(FIXED_RANDOM_SEED) - else: # pragma: no cover (py3) - fixed_random.seed(FIXED_RANDOM_SEED, version=1) + fixed_random.seed(FIXED_RANDOM_SEED, version=1) seq = list(seq) random.shuffle(seq, random=fixed_random.random) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index f5bc9bfaa..e0066a265 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import contextlib import os import sys diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index ae1fa90ec..07cfaf128 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import argparse import re import sys @@ -22,7 +19,7 @@ def _process_filename_by_line(pattern, filename): for line_no, line in enumerate(f, start=1): if pattern.search(line): retv = 1 - output.write('{}:{}:'.format(filename, line_no)) + output.write(f'{filename}:{line_no}:') output.write_line(line.rstrip(b'\r\n')) return retv diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 6eecc0c83..f7ff3aa2d 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import contextlib import os import sys diff --git a/pre_commit/languages/python_venv.py b/pre_commit/languages/python_venv.py index ef9043fc6..a1edf9123 100644 --- a/pre_commit/languages/python_venv.py +++ b/pre_commit/languages/python_venv.py @@ -1,7 +1,4 @@ -from __future__ import unicode_literals - import os.path -import sys from pre_commit.languages import python from pre_commit.util import CalledProcessError @@ -13,10 +10,7 @@ def get_default_version(): # pragma: no cover (version specific) - if sys.version_info < (3,): - return 'python3' - else: - return python.get_default_version() + return python.get_default_version() def orig_py_exe(exe): # pragma: no cover (platform specific) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 83e2a6faf..85d9cedcd 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -1,7 +1,4 @@ -from __future__ import unicode_literals - import contextlib -import io import os.path import shutil import tarfile @@ -66,7 +63,7 @@ def _install_rbenv(prefix, version=C.DEFAULT): # pragma: windows no cover _extract_resource('ruby-build.tar.gz', plugins_dir) activate_path = prefix.path(directory, 'bin', 'activate') - with io.open(activate_path, 'w') as activate_file: + with open(activate_path, 'w') as activate_file: # This is similar to how you would install rbenv to your home directory # However we do a couple things to make the executables exposed and # configure it to work in our directory. @@ -86,7 +83,7 @@ def _install_rbenv(prefix, version=C.DEFAULT): # pragma: windows no cover # If we aren't using the system ruby, add a version here if version != C.DEFAULT: - activate_file.write('export RBENV_VERSION="{}"\n'.format(version)) + activate_file.write(f'export RBENV_VERSION="{version}"\n') def _install_ruby(prefix, version): # pragma: windows no cover diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 91291fb34..de3f6fdd9 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import contextlib import os.path @@ -85,7 +83,7 @@ def install_environment(prefix, version, additional_dependencies): for package in packages_to_install: cmd_output_b( 'cargo', 'install', '--bins', '--root', directory, *package, - cwd=prefix.prefix_dir + cwd=prefix.prefix_dir, ) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 96b8aeb6f..cd5005a9a 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from pre_commit.languages import helpers diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 014349596..902d752f2 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import contextlib import os diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index b412b368c..2d4d6390c 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from pre_commit.languages import helpers diff --git a/pre_commit/logging_handler.py b/pre_commit/logging_handler.py index a1e2c0864..0a679a9f5 100644 --- a/pre_commit/logging_handler.py +++ b/pre_commit/logging_handler.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import contextlib import logging @@ -19,14 +17,14 @@ class LoggingHandler(logging.Handler): def __init__(self, use_color): - super(LoggingHandler, self).__init__() + super().__init__() self.use_color = use_color def emit(self, record): output.write_line( '{} {}'.format( color.format_color( - '[{}]'.format(record.levelname), + f'[{record.levelname}]', LOG_LEVEL_COLORS[record.levelname], self.use_color, ), diff --git a/pre_commit/main.py b/pre_commit/main.py index 8ae145a8b..467d1fbf8 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import argparse import logging import os @@ -57,7 +55,7 @@ def _add_config_option(parser): class AppendReplaceDefault(argparse.Action): def __init__(self, *args, **kwargs): - super(AppendReplaceDefault, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.appended = False def __call__(self, parser, namespace, values, option_string=None): @@ -154,7 +152,7 @@ def main(argv=None): parser.add_argument( '-V', '--version', action='version', - version='%(prog)s {}'.format(C.VERSION), + version=f'%(prog)s {C.VERSION}', ) subparsers = parser.add_subparsers(dest='command') @@ -254,7 +252,7 @@ def main(argv=None): _add_run_options(run_parser) sample_config_parser = subparsers.add_parser( - 'sample-config', help='Produce a sample {} file'.format(C.CONFIG_FILE), + 'sample-config', help=f'Produce a sample {C.CONFIG_FILE} file', ) _add_color_option(sample_config_parser) _add_config_option(sample_config_parser) @@ -345,11 +343,11 @@ def main(argv=None): return uninstall(hook_types=args.hook_types) else: raise NotImplementedError( - 'Command {} not implemented.'.format(args.command), + f'Command {args.command} not implemented.', ) raise AssertionError( - 'Command {} failed to exit with a returncode'.format(args.command), + f'Command {args.command} failed to exit with a returncode', ) diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index 1542548dc..5a9f81648 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import argparse import os.path import tarfile @@ -59,7 +55,7 @@ def main(argv=None): args = parser.parse_args(argv) for archive_name, repo, ref in REPOS: output.write_line( - 'Making {}.tar.gz for {}@{}'.format(archive_name, repo, ref), + f'Making {archive_name}.tar.gz for {repo}@{ref}', ) make_archive(archive_name, repo, ref, args.dest) diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index b1ccdac3d..ef6c9ead5 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -16,7 +16,7 @@ def check_all_hooks_match_files(config_file): if hook.always_run or hook.language == 'fail': continue elif not classifier.filenames_for_hook(hook): - print('{} does not apply to this repository'.format(hook.id)) + print(f'{hook.id} does not apply to this repository') retv = 1 return retv diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index c4860db33..f22ff902f 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import argparse import re diff --git a/pre_commit/output.py b/pre_commit/output.py index 478ad5e65..6ca0b3785 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import sys from pre_commit import color diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index ab2c9eec6..8e99bec96 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import os.path from identify.identify import parse_shebang_from_file @@ -44,7 +41,7 @@ def find_executable(exe, _environ=None): def normexe(orig): def _error(msg): - raise ExecutableNotFoundError('Executable `{}` {}'.format(orig, msg)) + raise ExecutableNotFoundError(f'Executable `{orig}` {msg}') if os.sep not in orig and (not os.altsep or os.altsep not in orig): exe = find_executable(orig) diff --git a/pre_commit/prefix.py b/pre_commit/prefix.py index f8a8a9d69..17699a3fd 100644 --- a/pre_commit/prefix.py +++ b/pre_commit/prefix.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import collections import os.path diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 829fe47ca..186f1e4ef 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -1,7 +1,4 @@ -from __future__ import unicode_literals - import collections -import io import json import logging import os @@ -36,14 +33,14 @@ def _read_state(prefix, venv): if not os.path.exists(filename): return None else: - with io.open(filename) as f: + with open(filename) as f: return json.load(f) def _write_state(prefix, venv, state): state_filename = _state_filename(prefix, venv) staging = state_filename + 'staging' - with io.open(staging, 'w') as state_file: + with open(staging, 'w') as state_file: state_file.write(five.to_text(json.dumps(state))) # Move the file into place atomically to indicate we've installed os.rename(staging, state_filename) @@ -82,7 +79,7 @@ def installed(self): ) def install(self): - logger.info('Installing environment for {}.'.format(self.src)) + logger.info(f'Installing environment for {self.src}.') logger.info('Once installed this environment will be reused.') logger.info('This may take a few minutes...') diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 81ffc955c..e83c126ac 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -1,7 +1,5 @@ #!/usr/bin/env python3 """File generated by pre-commit: https://pre-commit.com""" -from __future__ import print_function - import distutils.spawn import os import subprocess @@ -64,7 +62,7 @@ def _run_legacy(): else: stdin = None - legacy_hook = os.path.join(HERE, '{}.legacy'.format(HOOK_TYPE)) + legacy_hook = os.path.join(HERE, f'{HOOK_TYPE}.legacy') if os.access(legacy_hook, os.X_OK): cmd = _norm_exe(legacy_hook) + (legacy_hook,) + tuple(sys.argv[1:]) proc = subprocess.Popen(cmd, stdin=subprocess.PIPE if stdin else None) @@ -136,7 +134,7 @@ def _pre_push(stdin): # ancestors not found in remote ancestors = subprocess.check_output(( 'git', 'rev-list', local_sha, '--topo-order', '--reverse', - '--not', '--remotes={}'.format(remote), + '--not', f'--remotes={remote}', )).decode().strip() if not ancestors: continue @@ -148,7 +146,7 @@ def _pre_push(stdin): # pushing the whole tree including root commit opts = ('--all-files',) else: - cmd = ('git', 'rev-parse', '{}^'.format(first_ancestor)) + cmd = ('git', 'rev-parse', f'{first_ancestor}^') source = subprocess.check_output(cmd).decode().strip() opts = ('--origin', local_sha, '--source', source) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 5bb841547..bb81424fd 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -1,7 +1,4 @@ -from __future__ import unicode_literals - import contextlib -import io import logging import os.path import time @@ -54,11 +51,11 @@ def _unstaged_changes_cleared(patch_dir): patch_filename = os.path.join(patch_dir, patch_filename) logger.warning('Unstaged files detected.') logger.info( - 'Stashing unstaged files to {}.'.format(patch_filename), + f'Stashing unstaged files to {patch_filename}.', ) # Save the current unstaged changes as a patch mkdirp(patch_dir) - with io.open(patch_filename, 'wb') as patch_file: + with open(patch_filename, 'wb') as patch_file: patch_file.write(diff_stdout_binary) # Clear the working directory of unstaged changes @@ -79,7 +76,7 @@ def _unstaged_changes_cleared(patch_dir): # Roll back the changes made by hooks. cmd_output_b('git', 'checkout', '--', '.') _git_apply(patch_filename) - logger.info('Restored changes from {}.'.format(patch_filename)) + logger.info(f'Restored changes from {patch_filename}.') else: # There weren't any staged files so we don't need to do anything # special diff --git a/pre_commit/store.py b/pre_commit/store.py index d9b674b27..e342e393d 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -1,7 +1,4 @@ -from __future__ import unicode_literals - import contextlib -import io import logging import os.path import sqlite3 @@ -34,7 +31,7 @@ def _get_default_directory(): ) -class Store(object): +class Store: get_default_directory = staticmethod(_get_default_directory) def __init__(self, directory=None): @@ -43,7 +40,7 @@ def __init__(self, directory=None): if not os.path.exists(self.directory): mkdirp(self.directory) - with io.open(os.path.join(self.directory, 'README'), 'w') as f: + with open(os.path.join(self.directory, 'README'), 'w') as f: f.write( 'This directory is maintained by the pre-commit project.\n' 'Learn more: https://github.com/pre-commit/pre-commit\n', @@ -122,7 +119,7 @@ def _get_result(): if result: # pragma: no cover (race) return result - logger.info('Initializing environment for {}.'.format(repo)) + logger.info(f'Initializing environment for {repo}.') directory = tempfile.mkdtemp(prefix='repo', dir=self.directory) with clean_path_on_failure(directory): @@ -179,8 +176,8 @@ def _git_cmd(*args): def make_local(self, deps): def make_local_strategy(directory): for resource in self.LOCAL_RESOURCES: - contents = resource_text('empty_template_{}'.format(resource)) - with io.open(os.path.join(directory, resource), 'w') as f: + contents = resource_text(f'empty_template_{resource}') + with open(os.path.join(directory, resource), 'w') as f: f.write(contents) env = git.no_git_env() diff --git a/pre_commit/util.py b/pre_commit/util.py index 8072042b9..2c4d87baa 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import contextlib import errno import os.path @@ -9,8 +7,6 @@ import sys import tempfile -import six - from pre_commit import five from pre_commit import parse_shebang @@ -75,7 +71,7 @@ def make_executable(filename): class CalledProcessError(RuntimeError): def __init__(self, returncode, cmd, expected_returncode, stdout, stderr): - super(CalledProcessError, self).__init__( + super().__init__( returncode, cmd, expected_returncode, stdout, stderr, ) self.returncode = returncode @@ -104,12 +100,8 @@ def _indent_or_none(part): def to_text(self): return self.to_bytes().decode('UTF-8') - if six.PY2: # pragma: no cover (py2) - __str__ = to_bytes - __unicode__ = to_text - else: # pragma: no cover (py3) - __bytes__ = to_bytes - __str__ = to_text + __bytes__ = to_bytes + __str__ = to_text def _cmd_kwargs(*cmd, **kwargs): @@ -154,7 +146,7 @@ def cmd_output(*cmd, **kwargs): from os import openpty import termios - class Pty(object): + class Pty: def __init__(self): self.r = self.w = None diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index ace82f5a3..d5d13746c 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import division -from __future__ import unicode_literals - import concurrent.futures import contextlib import math @@ -9,8 +5,6 @@ import subprocess import sys -import six - from pre_commit import parse_shebang from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_p @@ -26,7 +20,7 @@ def _environ_size(_env=None): def _get_platform_max_length(): # pragma: no cover (platform specific) if os.name == 'posix': - maximum = os.sysconf(str('SC_ARG_MAX')) - 2048 - _environ_size() + maximum = os.sysconf('SC_ARG_MAX') - 2048 - _environ_size() maximum = max(min(maximum, 2 ** 17), 2 ** 12) return maximum elif os.name == 'nt': @@ -43,10 +37,7 @@ def _command_length(*cmd): # https://github.com/pre-commit/pre-commit/pull/839 if sys.platform == 'win32': # the python2.x apis require bytes, we encode as UTF-8 - if six.PY2: - return len(full_cmd.encode('utf-8')) - else: - return len(full_cmd.encode('utf-16le')) // 2 + return len(full_cmd.encode('utf-16le')) // 2 else: return len(full_cmd.encode(sys.getfilesystemencoding())) @@ -125,7 +116,7 @@ def xargs(cmd, varargs, **kwargs): def run_cmd_partition(run_cmd): return cmd_fn( - *run_cmd, retcode=None, stderr=subprocess.STDOUT, **kwargs + *run_cmd, retcode=None, stderr=subprocess.STDOUT, **kwargs, ) threads = min(len(partitions), target_concurrency) diff --git a/setup.cfg b/setup.cfg index 38b26ee8e..daca858ad 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,10 +11,8 @@ license = MIT license_file = LICENSE classifiers = License :: OSI Approved :: MIT License - Programming Language :: Python :: 2 - Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 @@ -32,10 +30,9 @@ install_requires = six toml virtualenv>=15.2 - futures;python_version<"3.2" importlib-metadata;python_version<"3.8" importlib-resources;python_version<"3.7" -python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* +python_requires = >=3.6 [options.entry_points] console_scripts = diff --git a/testing/auto_namedtuple.py b/testing/auto_namedtuple.py index 02e08fef0..0841094eb 100644 --- a/testing/auto_namedtuple.py +++ b/testing/auto_namedtuple.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import collections diff --git a/testing/fixtures.py b/testing/fixtures.py index 70d0750de..a9f54a22a 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -1,8 +1,4 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import contextlib -import io import os.path import shutil @@ -58,10 +54,10 @@ def modify_manifest(path, commit=True): .pre-commit-hooks.yaml. """ manifest_path = os.path.join(path, C.MANIFEST_FILE) - with io.open(manifest_path) as f: + with open(manifest_path) as f: manifest = ordered_load(f.read()) yield manifest - with io.open(manifest_path, 'w') as manifest_file: + with open(manifest_path, 'w') as manifest_file: manifest_file.write(ordered_dump(manifest, **C.YAML_DUMP_KWARGS)) if commit: git_commit(msg=modify_manifest.__name__, cwd=path) @@ -73,10 +69,10 @@ def modify_config(path='.', commit=True): .pre-commit-config.yaml """ config_path = os.path.join(path, C.CONFIG_FILE) - with io.open(config_path) as f: + with open(config_path) as f: config = ordered_load(f.read()) yield config - with io.open(config_path, 'w', encoding='UTF-8') as config_file: + with open(config_path, 'w', encoding='UTF-8') as config_file: config_file.write(ordered_dump(config, **C.YAML_DUMP_KWARGS)) if commit: git_commit(msg=modify_config.__name__, cwd=path) @@ -101,7 +97,7 @@ def sample_meta_config(): def make_config_from_repo(repo_path, rev=None, hooks=None, check=True): manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) config = { - 'repo': 'file://{}'.format(repo_path), + 'repo': f'file://{repo_path}', 'rev': rev or git.head_rev(repo_path), 'hooks': hooks or [{'id': hook['id']} for hook in manifest], } @@ -117,7 +113,7 @@ def make_config_from_repo(repo_path, rev=None, hooks=None, check=True): def read_config(directory, config_file=C.CONFIG_FILE): config_path = os.path.join(directory, config_file) - with io.open(config_path) as f: + with open(config_path) as f: config = ordered_load(f.read()) return config @@ -126,7 +122,7 @@ def write_config(directory, config, config_file=C.CONFIG_FILE): if type(config) is not list and 'repos' not in config: assert isinstance(config, dict), config config = {'repos': [config]} - with io.open(os.path.join(directory, config_file), 'w') as outfile: + with open(os.path.join(directory, config_file), 'w') as outfile: outfile.write(ordered_dump(config, **C.YAML_DUMP_KWARGS)) diff --git a/testing/resources/python3_hooks_repo/py3_hook.py b/testing/resources/python3_hooks_repo/py3_hook.py index f0f880886..8c9cda4c6 100644 --- a/testing/resources/python3_hooks_repo/py3_hook.py +++ b/testing/resources/python3_hooks_repo/py3_hook.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import sys diff --git a/testing/resources/python_hooks_repo/foo.py b/testing/resources/python_hooks_repo/foo.py index 412a5c625..9c4368e20 100644 --- a/testing/resources/python_hooks_repo/foo.py +++ b/testing/resources/python_hooks_repo/foo.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import sys diff --git a/testing/resources/python_venv_hooks_repo/foo.py b/testing/resources/python_venv_hooks_repo/foo.py index 412a5c625..9c4368e20 100644 --- a/testing/resources/python_venv_hooks_repo/foo.py +++ b/testing/resources/python_venv_hooks_repo/foo.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import sys diff --git a/testing/resources/stdout_stderr_repo/stdout-stderr-entry b/testing/resources/stdout_stderr_repo/stdout-stderr-entry index e382373dd..d383c191f 100755 --- a/testing/resources/stdout_stderr_repo/stdout-stderr-entry +++ b/testing/resources/stdout_stderr_repo/stdout-stderr-entry @@ -5,7 +5,7 @@ import sys def main(): for i in range(6): f = sys.stdout if i % 2 == 0 else sys.stderr - f.write('{}\n'.format(i)) + f.write(f'{i}\n') f.flush() diff --git a/testing/util.py b/testing/util.py index a2a2e24f3..dbe475eb9 100644 --- a/testing/util.py +++ b/testing/util.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import contextlib import os.path import subprocess @@ -50,7 +48,7 @@ def broken_deep_listdir(): # pragma: no cover (platform specific) if sys.platform != 'win32': return False try: - os.listdir(str('\\\\?\\') + os.path.abspath(str('.'))) + os.listdir('\\\\?\\' + os.path.abspath('.')) except OSError: return True try: diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 6174889a3..8499c3dda 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import logging import cfgv diff --git a/tests/color_test.py b/tests/color_test.py index 6c9889d1b..4c4928147 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import sys import mock @@ -14,7 +12,7 @@ @pytest.mark.parametrize( ('in_text', 'in_color', 'in_use_color', 'expected'), ( - ('foo', GREEN, True, '{}foo\033[0m'.format(GREEN)), + ('foo', GREEN, True, f'{GREEN}foo\033[0m'), ('foo', GREEN, False, 'foo'), ), ) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index f8ea084e0..b126cff7c 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import pipes import pytest @@ -213,7 +211,7 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( with open(C.CONFIG_FILE) as f: before = f.read() - repo_name = 'file://{}'.format(out_of_date.path) + repo_name = f'file://{out_of_date.path}' ret = autoupdate( C.CONFIG_FILE, store, freeze=False, tags_only=False, repos=(repo_name,), @@ -312,7 +310,7 @@ def test_autoupdate_freeze(tagged, in_tmpdir, store): assert autoupdate(C.CONFIG_FILE, store, freeze=True, tags_only=False) == 0 with open(C.CONFIG_FILE) as f: - expected = 'rev: {} # frozen: v1.2.3'.format(tagged.head_rev) + expected = f'rev: {tagged.head_rev} # frozen: v1.2.3' assert expected in f.read() # if we un-freeze it should remove the frozen comment diff --git a/tests/commands/clean_test.py b/tests/commands/clean_test.py index dc33ebb07..22fe974cd 100644 --- a/tests/commands/clean_test.py +++ b/tests/commands/clean_test.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import os.path import mock diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index f0e170973..73d053008 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -1,8 +1,3 @@ -# -*- coding: UTF-8 -*- -from __future__ import absolute_import -from __future__ import unicode_literals - -import io import os.path import re import sys @@ -123,7 +118,7 @@ def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): fn=cmd_output_mocked_pre_commit_home, retcode=None, tempdir_factory=tempdir_factory, - **kwargs + **kwargs, ) @@ -203,7 +198,7 @@ def test_commit_am(tempdir_factory, store): open('unstaged', 'w').close() cmd_output('git', 'add', '.') git_commit(cwd=path) - with io.open('unstaged', 'w') as foo_file: + with open('unstaged', 'w') as foo_file: foo_file.write('Oh hai') assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 @@ -314,7 +309,7 @@ def test_failing_hooks_returns_nonzero(tempdir_factory, store): def _write_legacy_hook(path): mkdirp(os.path.join(path, '.git/hooks')) - with io.open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: + with open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: f.write('#!/usr/bin/env bash\necho "legacy hook"\n') make_executable(f.name) @@ -377,7 +372,7 @@ def test_failing_existing_hook_returns_1(tempdir_factory, store): with cwd(path): # Write out a failing "old" hook mkdirp(os.path.join(path, '.git/hooks')) - with io.open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: + with open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: f.write('#!/usr/bin/env bash\necho "fail!"\nexit 1\n') make_executable(f.name) @@ -439,7 +434,7 @@ def test_replace_old_commit_script(tempdir_factory, store): ) mkdirp(os.path.join(path, '.git/hooks')) - with io.open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: + with open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: f.write(new_contents) make_executable(f.name) @@ -525,7 +520,7 @@ def _get_push_output(tempdir_factory, opts=()): return cmd_output_mocked_pre_commit_home( 'git', 'push', 'origin', 'HEAD:new_branch', *opts, tempdir_factory=tempdir_factory, - retcode=None + retcode=None, )[:2] @@ -616,7 +611,7 @@ def test_pre_push_legacy(tempdir_factory, store): cmd_output('git', 'clone', upstream, path) with cwd(path): mkdirp(os.path.join(path, '.git/hooks')) - with io.open(os.path.join(path, '.git/hooks/pre-push'), 'w') as f: + with open(os.path.join(path, '.git/hooks/pre-push'), 'w') as f: f.write( '#!/usr/bin/env bash\n' 'set -eu\n' @@ -665,7 +660,7 @@ def test_commit_msg_integration_passing( def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): hook_path = os.path.join(commit_msg_repo, '.git/hooks/commit-msg') mkdirp(os.path.dirname(hook_path)) - with io.open(hook_path, 'w') as hook_file: + with open(hook_path, 'w') as hook_file: hook_file.write( '#!/usr/bin/env bash\n' 'set -eu\n' @@ -709,7 +704,7 @@ def test_prepare_commit_msg_integration_passing( commit_msg_path = os.path.join( prepare_commit_msg_repo, '.git/COMMIT_EDITMSG', ) - with io.open(commit_msg_path) as f: + with open(commit_msg_path) as f: assert 'Signed off by: ' in f.read() @@ -720,7 +715,7 @@ def test_prepare_commit_msg_legacy( prepare_commit_msg_repo, '.git/hooks/prepare-commit-msg', ) mkdirp(os.path.dirname(hook_path)) - with io.open(hook_path, 'w') as hook_file: + with open(hook_path, 'w') as hook_file: hook_file.write( '#!/usr/bin/env bash\n' 'set -eu\n' @@ -739,7 +734,7 @@ def test_prepare_commit_msg_legacy( commit_msg_path = os.path.join( prepare_commit_msg_repo, '.git/COMMIT_EDITMSG', ) - with io.open(commit_msg_path) as f: + with open(commit_msg_path) as f: assert 'Signed off by: ' in f.read() diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index c58b9f74b..efc0d1cb4 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import pytest import pre_commit.constants as C diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 58d40fe3b..03962a7cd 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -1,7 +1,3 @@ -# -*- coding: UTF-8 -*- -from __future__ import unicode_literals - -import io import os.path import pipes import sys @@ -154,7 +150,7 @@ def test_types_hook_repository(cap_out, store, tempdir_factory): def test_exclude_types_hook_repository(cap_out, store, tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'exclude_types_repo') with cwd(git_path): - with io.open('exe', 'w') as exe: + with open('exe', 'w') as exe: exe.write('#!/usr/bin/env python3\n') make_executable('exe') cmd_output('git', 'add', 'exe') @@ -601,8 +597,8 @@ def test_stages(cap_out, store, repo_with_passing_hook): 'repo': 'local', 'hooks': [ { - 'id': 'do-not-commit-{}'.format(i), - 'name': 'hook {}'.format(i), + 'id': f'do-not-commit-{i}', + 'name': f'hook {i}', 'entry': 'DO NOT COMMIT', 'language': 'pygrep', 'stages': [stage], @@ -636,7 +632,7 @@ def _run_for_stage(stage): def test_commit_msg_hook(cap_out, store, commit_msg_repo): filename = '.git/COMMIT_EDITMSG' - with io.open(filename, 'w') as f: + with open(filename, 'w') as f: f.write('This is the commit message') _test_run( @@ -652,7 +648,7 @@ def test_commit_msg_hook(cap_out, store, commit_msg_repo): def test_prepare_commit_msg_hook(cap_out, store, prepare_commit_msg_repo): filename = '.git/COMMIT_EDITMSG' - with io.open(filename, 'w') as f: + with open(filename, 'w') as f: f.write('This is the commit message') _test_run( @@ -665,7 +661,7 @@ def test_prepare_commit_msg_hook(cap_out, store, prepare_commit_msg_repo): stage=False, ) - with io.open(filename) as f: + with open(filename) as f: assert 'Signed off by: ' in f.read() @@ -692,7 +688,7 @@ def test_local_hook_passes(cap_out, store, repo_with_passing_hook): } add_config_to_repo(repo_with_passing_hook, config) - with io.open('dummy.py', 'w') as staged_file: + with open('dummy.py', 'w') as staged_file: staged_file.write('"""TODO: something"""\n') cmd_output('git', 'add', 'dummy.py') @@ -719,7 +715,7 @@ def test_local_hook_fails(cap_out, store, repo_with_passing_hook): } add_config_to_repo(repo_with_passing_hook, config) - with io.open('dummy.py', 'w') as staged_file: + with open('dummy.py', 'w') as staged_file: staged_file.write('"""TODO: something"""\n') cmd_output('git', 'add', 'dummy.py') diff --git a/tests/commands/sample_config_test.py b/tests/commands/sample_config_test.py index 57ef3a494..11c087649 100644 --- a/tests/commands/sample_config_test.py +++ b/tests/commands/sample_config_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - from pre_commit.commands.sample_config import sample_config diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index 1849c70a5..db2c47ba6 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import os.path import re import time diff --git a/tests/conftest.py b/tests/conftest.py index 6e9fcf23c..0018cfd41 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import functools import io import logging @@ -8,7 +5,6 @@ import mock import pytest -import six from pre_commit import output from pre_commit.envcontext import envcontext @@ -36,19 +32,19 @@ def no_warnings(recwarn): ' missing __init__' in message ): warnings.append( - '{}:{} {}'.format(warning.filename, warning.lineno, message), + f'{warning.filename}:{warning.lineno} {message}', ) assert not warnings @pytest.fixture def tempdir_factory(tmpdir): - class TmpdirFactory(object): + class TmpdirFactory: def __init__(self): self.tmpdir_count = 0 def get(self): - path = tmpdir.join(six.text_type(self.tmpdir_count)).strpath + path = tmpdir.join(str(self.tmpdir_count)).strpath self.tmpdir_count += 1 os.mkdir(path) return path @@ -73,18 +69,18 @@ def in_git_dir(tmpdir): def _make_conflict(): cmd_output('git', 'checkout', 'origin/master', '-b', 'foo') - with io.open('conflict_file', 'w') as conflict_file: + with open('conflict_file', 'w') as conflict_file: conflict_file.write('herp\nderp\n') cmd_output('git', 'add', 'conflict_file') - with io.open('foo_only_file', 'w') as foo_only_file: + with open('foo_only_file', 'w') as foo_only_file: foo_only_file.write('foo') cmd_output('git', 'add', 'foo_only_file') git_commit(msg=_make_conflict.__name__) cmd_output('git', 'checkout', 'origin/master', '-b', 'bar') - with io.open('conflict_file', 'w') as conflict_file: + with open('conflict_file', 'w') as conflict_file: conflict_file.write('harp\nddrp\n') cmd_output('git', 'add', 'conflict_file') - with io.open('bar_only_file', 'w') as bar_only_file: + with open('bar_only_file', 'w') as bar_only_file: bar_only_file.write('bar') cmd_output('git', 'add', 'bar_only_file') git_commit(msg=_make_conflict.__name__) @@ -145,14 +141,14 @@ def prepare_commit_msg_repo(tempdir_factory): 'hooks': [{ 'id': 'add-signoff', 'name': 'Add "Signed off by:"', - 'entry': './{}'.format(script_name), + 'entry': f'./{script_name}', 'language': 'script', 'stages': ['prepare-commit-msg'], }], } write_config(path, config) with cwd(path): - with io.open(script_name, 'w') as script_file: + with open(script_name, 'w') as script_file: script_file.write( '#!/usr/bin/env bash\n' 'set -eu\n' @@ -229,7 +225,7 @@ def log_info_mock(): yield mck -class FakeStream(object): +class FakeStream: def __init__(self): self.data = io.BytesIO() @@ -240,7 +236,7 @@ def flush(self): pass -class Fixture(object): +class Fixture: def __init__(self, stream): self._stream = stream diff --git a/tests/envcontext_test.py b/tests/envcontext_test.py index c03e94317..7c4bdddd0 100644 --- a/tests/envcontext_test.py +++ b/tests/envcontext_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import os import mock diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 74ade6189..403dcfbd1 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -1,8 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import unicode_literals - -import io import os.path import re import sys @@ -109,7 +104,7 @@ def test_log_and_exit(cap_out, mock_store_dir): ) assert os.path.exists(log_file) - with io.open(log_file) as f: + with open(log_file) as f: logged = f.read() expected = ( r'^### version information\n' @@ -158,4 +153,4 @@ def test_error_handler_no_tty(tempdir_factory): log_file = os.path.join(pre_commit_home, 'pre-commit.log') out_lines = out.splitlines() assert out_lines[-2] == 'An unexpected error has occurred: ValueError: ☃' - assert out_lines[-1] == 'Check the log at {}'.format(log_file) + assert out_lines[-1] == f'Check the log at {log_file}' diff --git a/tests/git_test.py b/tests/git_test.py index 299729dbc..4a5bfb9be 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import unicode_literals - import os.path import pytest diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index 2185ae0d2..e226d18ff 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -1,26 +1,17 @@ -from __future__ import unicode_literals - import functools import inspect import pytest -import six from pre_commit.languages.all import all_languages from pre_commit.languages.all import languages -if six.PY2: # pragma: no cover - ArgSpec = functools.partial( - inspect.ArgSpec, varargs=None, keywords=None, defaults=None, - ) - getargspec = inspect.getargspec -else: # pragma: no cover - ArgSpec = functools.partial( - inspect.FullArgSpec, varargs=None, varkw=None, defaults=None, - kwonlyargs=[], kwonlydefaults=None, annotations={}, - ) - getargspec = inspect.getfullargspec +ArgSpec = functools.partial( + inspect.FullArgSpec, varargs=None, varkw=None, defaults=None, + kwonlyargs=[], kwonlydefaults=None, annotations={}, +) +getargspec = inspect.getfullargspec @pytest.mark.parametrize('language', all_languages) diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 4ea767917..89e57000b 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import mock from pre_commit.languages import docker diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py index 483f41ead..9a64ed195 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import pytest from pre_commit.languages.golang import guess_go_dir diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index 629322c37..6f1232b43 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import multiprocessing import os import sys diff --git a/tests/languages/pygrep_test.py b/tests/languages/pygrep_test.py index d91363e2f..cabea22ec 100644 --- a/tests/languages/pygrep_test.py +++ b/tests/languages/pygrep_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import pytest from pre_commit.languages import pygrep diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 55854a8a7..d806953e9 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import os.path import sys @@ -16,7 +13,7 @@ def test_norm_version_expanduser(): home = os.path.expanduser('~') if os.name == 'nt': # pragma: no cover (nt) path = r'~\python343' - expected_path = r'{}\python343'.format(home) + expected_path = fr'{home}\python343' else: # pragma: windows no cover path = '~/.pyenv/versions/3.4.3/bin/python' expected_path = home + '/.pyenv/versions/3.4.3/bin/python' diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index a0b4cfd4b..497b01d65 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import os.path import pipes diff --git a/tests/logging_handler_test.py b/tests/logging_handler_test.py index 0e72541a2..0c2d96f3c 100644 --- a/tests/logging_handler_test.py +++ b/tests/logging_handler_test.py @@ -1,10 +1,8 @@ -from __future__ import unicode_literals - from pre_commit import color from pre_commit.logging_handler import LoggingHandler -class FakeLogRecord(object): +class FakeLogRecord: def __init__(self, message, levelname, levelno): self.message = message self.levelname = levelname diff --git a/tests/main_test.py b/tests/main_test.py index c2c7a8657..107a2e67d 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import argparse import os.path @@ -27,7 +24,7 @@ def test_append_replace_default(argv, expected): assert parser.parse_args(argv).f == expected -class Args(object): +class Args: def __init__(self, **kwargs): kwargs.setdefault('command', 'help') kwargs.setdefault('config', C.CONFIG_FILE) @@ -189,4 +186,4 @@ def test_expected_fatal_error_no_git_repo(in_tmpdir, cap_out, mock_store_dir): 'An error has occurred: FatalError: git failed. ' 'Is it installed, and are you in a Git repository directory?' ) - assert cap_out_lines[-1] == 'Check the log at {}'.format(log_file) + assert cap_out_lines[-1] == f'Check the log at {log_file}' diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py index 52c9c9b6f..6ae2f8e74 100644 --- a/tests/make_archives_test.py +++ b/tests/make_archives_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import tarfile from pre_commit import git @@ -46,4 +43,4 @@ def test_main(tmpdir): make_archives.main(('--dest', tmpdir.strpath)) for archive, _, _ in make_archives.REPOS: - assert tmpdir.join('{}.tar.gz'.format(archive)).exists() + assert tmpdir.join(f'{archive}.tar.gz').exists() diff --git a/tests/output_test.py b/tests/output_test.py index 8b6ea90d8..4c641c85e 100644 --- a/tests/output_test.py +++ b/tests/output_test.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import mock import pytest diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index 84ace31c9..5798c4e24 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -1,9 +1,5 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import contextlib import distutils.spawn -import io import os import sys @@ -42,8 +38,8 @@ def test_find_executable_not_found_none(): def write_executable(shebang, filename='run'): os.mkdir('bin') path = os.path.join('bin', filename) - with io.open(path, 'w') as f: - f.write('#!{}'.format(shebang)) + with open(path, 'w') as f: + f.write(f'#!{shebang}') make_executable(path) return path @@ -106,7 +102,7 @@ def test_normexe_is_a_directory(tmpdir): with pytest.raises(OSError) as excinfo: parse_shebang.normexe(exe) msg, = excinfo.value.args - assert msg == 'Executable `{}` is a directory'.format(exe) + assert msg == f'Executable `{exe}` is a directory' def test_normexe_already_full_path(): diff --git a/tests/prefix_test.py b/tests/prefix_test.py index 2806cff1a..6ce8be127 100644 --- a/tests/prefix_test.py +++ b/tests/prefix_test.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import os.path import pytest diff --git a/tests/repository_test.py b/tests/repository_test.py index 1f06b355a..1f5521b86 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import os.path import re import shutil @@ -473,7 +470,7 @@ def _norm_pwd(path): # Under windows bash's temp and windows temp is different. # This normalizes to the bash /tmp return cmd_output_b( - 'bash', '-c', "cd '{}' && pwd".format(path), + 'bash', '-c', f"cd '{path}' && pwd", )[1].strip() @@ -844,7 +841,7 @@ def test_manifest_hooks(tempdir_factory, store): hook = _get_hook(config, store, 'bash_hook') assert hook == Hook( - src='file://{}'.format(path), + src=f'file://{path}', prefix=Prefix(mock.ANY), additional_dependencies=[], alias='', diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 107c14914..46e350e18 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -1,8 +1,3 @@ -# -*- coding: UTF-8 -*- -from __future__ import absolute_import -from __future__ import unicode_literals - -import io import itertools import os.path import shutil @@ -47,7 +42,7 @@ def _test_foo_state( encoding='UTF-8', ): assert os.path.exists(path.foo_filename) - with io.open(path.foo_filename, encoding=encoding) as f: + with open(path.foo_filename, encoding=encoding) as f: assert f.read() == foo_contents actual_status = get_short_git_status()['foo'] assert status == actual_status @@ -64,7 +59,7 @@ def test_foo_nothing_unstaged(foo_staged, patch_dir): def test_foo_something_unstaged(foo_staged, patch_dir): - with io.open(foo_staged.foo_filename, 'w') as foo_file: + with open(foo_staged.foo_filename, 'w') as foo_file: foo_file.write('herp\nderp\n') _test_foo_state(foo_staged, 'herp\nderp\n', 'AM') @@ -76,7 +71,7 @@ def test_foo_something_unstaged(foo_staged, patch_dir): def test_does_not_crash_patch_dir_does_not_exist(foo_staged, patch_dir): - with io.open(foo_staged.foo_filename, 'w') as foo_file: + with open(foo_staged.foo_filename, 'w') as foo_file: foo_file.write('hello\nworld\n') shutil.rmtree(patch_dir) @@ -97,7 +92,7 @@ def test_foo_something_unstaged_diff_color_always(foo_staged, patch_dir): def test_foo_both_modify_non_conflicting(foo_staged, patch_dir): - with io.open(foo_staged.foo_filename, 'w') as foo_file: + with open(foo_staged.foo_filename, 'w') as foo_file: foo_file.write(FOO_CONTENTS + '9\n') _test_foo_state(foo_staged, FOO_CONTENTS + '9\n', 'AM') @@ -106,7 +101,7 @@ def test_foo_both_modify_non_conflicting(foo_staged, patch_dir): _test_foo_state(foo_staged) # Modify the file as part of the "pre-commit" - with io.open(foo_staged.foo_filename, 'w') as foo_file: + with open(foo_staged.foo_filename, 'w') as foo_file: foo_file.write(FOO_CONTENTS.replace('1', 'a')) _test_foo_state(foo_staged, FOO_CONTENTS.replace('1', 'a'), 'AM') @@ -115,7 +110,7 @@ def test_foo_both_modify_non_conflicting(foo_staged, patch_dir): def test_foo_both_modify_conflicting(foo_staged, patch_dir): - with io.open(foo_staged.foo_filename, 'w') as foo_file: + with open(foo_staged.foo_filename, 'w') as foo_file: foo_file.write(FOO_CONTENTS.replace('1', 'a')) _test_foo_state(foo_staged, FOO_CONTENTS.replace('1', 'a'), 'AM') @@ -124,7 +119,7 @@ def test_foo_both_modify_conflicting(foo_staged, patch_dir): _test_foo_state(foo_staged) # Modify in the same place as the stashed diff - with io.open(foo_staged.foo_filename, 'w') as foo_file: + with open(foo_staged.foo_filename, 'w') as foo_file: foo_file.write(FOO_CONTENTS.replace('1', 'b')) _test_foo_state(foo_staged, FOO_CONTENTS.replace('1', 'b'), 'AM') @@ -142,8 +137,8 @@ def img_staged(in_git_dir): def _test_img_state(path, expected_file='img1.jpg', status='A'): assert os.path.exists(path.img_filename) - with io.open(path.img_filename, 'rb') as f1: - with io.open(get_resource_path(expected_file), 'rb') as f2: + with open(path.img_filename, 'rb') as f1: + with open(get_resource_path(expected_file), 'rb') as f2: assert f1.read() == f2.read() actual_status = get_short_git_status()['img.jpg'] assert status == actual_status @@ -248,7 +243,7 @@ def test_sub_something_unstaged(sub_staged, patch_dir): def test_stage_utf8_changes(foo_staged, patch_dir): contents = '\u2603' - with io.open('foo', 'w', encoding='UTF-8') as foo_file: + with open('foo', 'w', encoding='UTF-8') as foo_file: foo_file.write(contents) _test_foo_state(foo_staged, contents, 'AM') @@ -260,7 +255,7 @@ def test_stage_utf8_changes(foo_staged, patch_dir): def test_stage_non_utf8_changes(foo_staged, patch_dir): contents = 'ú' # Produce a latin-1 diff - with io.open('foo', 'w', encoding='latin-1') as foo_file: + with open('foo', 'w', encoding='latin-1') as foo_file: foo_file.write(contents) _test_foo_state(foo_staged, contents, 'AM', encoding='latin-1') @@ -282,14 +277,14 @@ def test_non_utf8_conflicting_diff(foo_staged, patch_dir): # Previously, the error message (though discarded immediately) was being # decoded with the UTF-8 codec (causing a crash) contents = 'ú \n' - with io.open('foo', 'w', encoding='latin-1') as foo_file: + with open('foo', 'w', encoding='latin-1') as foo_file: foo_file.write(contents) _test_foo_state(foo_staged, contents, 'AM', encoding='latin-1') with staged_files_only(patch_dir): _test_foo_state(foo_staged) # Create a conflicting diff that will need to be rolled back - with io.open('foo', 'w') as foo_file: + with open('foo', 'w') as foo_file: foo_file.write('') _test_foo_state(foo_staged, contents, 'AM', encoding='latin-1') diff --git a/tests/store_test.py b/tests/store_test.py index c71c35099..6fc8c0588 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -1,13 +1,8 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - -import io import os.path import sqlite3 import mock import pytest -import six from pre_commit import git from pre_commit.store import _get_default_directory @@ -53,7 +48,7 @@ def test_store_init(store): # Should create the store directory assert os.path.exists(store.directory) # Should create a README file indicating what the directory is about - with io.open(os.path.join(store.directory, 'README')) as readme_file: + with open(os.path.join(store.directory, 'README')) as readme_file: readme_contents = readme_file.read() for text_line in ( 'This directory is maintained by the pre-commit project.', @@ -93,7 +88,7 @@ def test_clone_cleans_up_on_checkout_failure(store): # This raises an exception because you can't clone something that # doesn't exist! store.clone('/i_dont_exist_lol', 'fake_rev') - assert '/i_dont_exist_lol' in six.text_type(excinfo.value) + assert '/i_dont_exist_lol' in str(excinfo.value) repo_dirs = [ d for d in os.listdir(store.directory) if d.startswith('repo') diff --git a/tests/util_test.py b/tests/util_test.py index 647fd1870..12373277e 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import os.path import stat import subprocess @@ -17,7 +15,7 @@ def test_CalledProcessError_str(): - error = CalledProcessError(1, [str('exe')], 0, b'output', b'errors') + error = CalledProcessError(1, ['exe'], 0, b'output', b'errors') assert str(error) == ( "command: ['exe']\n" 'return code: 1\n' @@ -30,7 +28,7 @@ def test_CalledProcessError_str(): def test_CalledProcessError_str_nooutput(): - error = CalledProcessError(1, [str('exe')], 0, b'', b'') + error = CalledProcessError(1, ['exe'], 0, b'', b'') assert str(error) == ( "command: ['exe']\n" 'return code: 1\n' diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 49bf70f60..c0bbe5238 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import unicode_literals - import concurrent.futures import os import sys diff --git a/tox.ini b/tox.ini index 1fac9332c..7fd0bf6ae 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py36,py37,pypy,pypy3,pre-commit +envlist = py36,py37,py38,pypy3,pre-commit [testenv] deps = -rrequirements-dev.txt From ab19b94811eadb3e8c05f16f39ca0a7f1012ebb3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 8 Jan 2020 21:23:18 -0800 Subject: [PATCH 518/544] some manual py2 cleanups --- pre_commit/five.py | 5 +--- pre_commit/util.py | 9 +++---- requirements-dev.txt | 1 - setup.cfg | 1 - .../python_venv_hooks_repo/foo/__init__.py | 0 tests/color_test.py | 2 +- tests/commands/clean_test.py | 2 +- tests/commands/init_templatedir_test.py | 3 +-- tests/commands/install_uninstall_test.py | 3 +-- tests/commands/run_test.py | 2 +- tests/commands/try_repo_test.py | 3 +-- tests/conftest.py | 2 +- tests/envcontext_test.py | 6 ++--- tests/error_handler_test.py | 2 +- tests/languages/all_test.py | 9 +++---- tests/languages/docker_test.py | 2 +- tests/languages/helpers_test.py | 2 +- tests/languages/python_test.py | 2 +- tests/main_test.py | 2 +- tests/output_test.py | 3 ++- tests/repository_test.py | 2 +- tests/store_test.py | 2 +- tests/xargs_test.py | 25 +++---------------- 23 files changed, 31 insertions(+), 59 deletions(-) delete mode 100644 testing/resources/python_venv_hooks_repo/foo/__init__.py diff --git a/pre_commit/five.py b/pre_commit/five.py index 8d9e5767d..7059b1639 100644 --- a/pre_commit/five.py +++ b/pre_commit/five.py @@ -1,6 +1,3 @@ -import six - - def to_text(s): return s if isinstance(s, str) else s.decode('UTF-8') @@ -9,4 +6,4 @@ def to_bytes(s): return s if isinstance(s, bytes) else s.encode('UTF-8') -n = to_bytes if six.PY2 else to_text +n = to_text diff --git a/pre_commit/util.py b/pre_commit/util.py index 2c4d87baa..8c9751b43 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -80,7 +80,7 @@ def __init__(self, returncode, cmd, expected_returncode, stdout, stderr): self.stdout = stdout self.stderr = stderr - def to_bytes(self): + def __bytes__(self): def _indent_or_none(part): if part: return b'\n ' + part.replace(b'\n', b'\n ') @@ -97,11 +97,8 @@ def _indent_or_none(part): b'stderr:', _indent_or_none(self.stderr), )) - def to_text(self): - return self.to_bytes().decode('UTF-8') - - __bytes__ = to_bytes - __str__ = to_text + def __str__(self): + return self.__bytes__().decode('UTF-8') def _cmd_kwargs(*cmd, **kwargs): diff --git a/requirements-dev.txt b/requirements-dev.txt index ba80df7f3..9dfea92d0 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,5 @@ -e . coverage -mock pytest pytest-env diff --git a/setup.cfg b/setup.cfg index daca858ad..bf666de68 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,7 +27,6 @@ install_requires = identify>=1.0.0 nodeenv>=0.11.1 pyyaml - six toml virtualenv>=15.2 importlib-metadata;python_version<"3.8" diff --git a/testing/resources/python_venv_hooks_repo/foo/__init__.py b/testing/resources/python_venv_hooks_repo/foo/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/color_test.py b/tests/color_test.py index 4c4928147..4d98bd8d6 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -1,6 +1,6 @@ import sys +from unittest import mock -import mock import pytest from pre_commit import envcontext diff --git a/tests/commands/clean_test.py b/tests/commands/clean_test.py index 22fe974cd..955a6bc4e 100644 --- a/tests/commands/clean_test.py +++ b/tests/commands/clean_test.py @@ -1,6 +1,6 @@ import os.path +from unittest import mock -import mock import pytest from pre_commit.commands.clean import clean diff --git a/tests/commands/init_templatedir_test.py b/tests/commands/init_templatedir_test.py index 12c6696a8..010638d56 100644 --- a/tests/commands/init_templatedir_test.py +++ b/tests/commands/init_templatedir_test.py @@ -1,6 +1,5 @@ import os.path - -import mock +from unittest import mock import pre_commit.constants as C from pre_commit.commands.init_templatedir import init_templatedir diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 73d053008..feef316e4 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -1,8 +1,7 @@ import os.path import re import sys - -import mock +from unittest import mock import pre_commit.constants as C from pre_commit.commands.install_uninstall import CURRENT_HASH diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 03962a7cd..d271575e7 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -2,8 +2,8 @@ import pipes import sys import time +from unittest import mock -import mock import pytest import pre_commit.constants as C diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index db2c47ba6..fca0f3dd1 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -1,8 +1,7 @@ import os.path import re import time - -import mock +from unittest import mock from pre_commit import git from pre_commit.commands.try_repo import try_repo diff --git a/tests/conftest.py b/tests/conftest.py index 0018cfd41..6993301e2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,8 +2,8 @@ import io import logging import os.path +from unittest import mock -import mock import pytest from pre_commit import output diff --git a/tests/envcontext_test.py b/tests/envcontext_test.py index 7c4bdddd0..81f25e381 100644 --- a/tests/envcontext_test.py +++ b/tests/envcontext_test.py @@ -1,6 +1,6 @@ import os +from unittest import mock -import mock import pytest from pre_commit.envcontext import envcontext @@ -91,11 +91,11 @@ def test_exception_safety(): class MyError(RuntimeError): pass - env = {} + env = {'hello': 'world'} with pytest.raises(MyError): with envcontext([('foo', 'bar')], _env=env): raise MyError() - assert env == {} + assert env == {'hello': 'world'} def test_integration_os_environ(): diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 403dcfbd1..fa2fc2d35 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -1,8 +1,8 @@ import os.path import re import sys +from unittest import mock -import mock import pytest from pre_commit import error_handler diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index e226d18ff..5e8c8253a 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -11,7 +11,6 @@ inspect.FullArgSpec, varargs=None, varkw=None, defaults=None, kwonlyargs=[], kwonlydefaults=None, annotations={}, ) -getargspec = inspect.getfullargspec @pytest.mark.parametrize('language', all_languages) @@ -19,7 +18,7 @@ def test_install_environment_argspec(language): expected_argspec = ArgSpec( args=['prefix', 'version', 'additional_dependencies'], ) - argspec = getargspec(languages[language].install_environment) + argspec = inspect.getfullargpsec(languages[language].install_environment) assert argspec == expected_argspec @@ -31,19 +30,19 @@ def test_ENVIRONMENT_DIR(language): @pytest.mark.parametrize('language', all_languages) def test_run_hook_argpsec(language): expected_argspec = ArgSpec(args=['hook', 'file_args', 'color']) - argspec = getargspec(languages[language].run_hook) + argspec = inspect.getfullargpsec(languages[language].run_hook) assert argspec == expected_argspec @pytest.mark.parametrize('language', all_languages) def test_get_default_version_argspec(language): expected_argspec = ArgSpec(args=[]) - argspec = getargspec(languages[language].get_default_version) + argspec = inspect.getfullargpsec(languages[language].get_default_version) assert argspec == expected_argspec @pytest.mark.parametrize('language', all_languages) def test_healthy_argspec(language): expected_argspec = ArgSpec(args=['prefix', 'language_version']) - argspec = getargspec(languages[language].healthy) + argspec = inspect.getfullargpsec(languages[language].healthy) assert argspec == expected_argspec diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 89e57000b..9d69a13d9 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -1,4 +1,4 @@ -import mock +from unittest import mock from pre_commit.languages import docker from pre_commit.util import CalledProcessError diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index 6f1232b43..b289f7259 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -1,8 +1,8 @@ import multiprocessing import os import sys +from unittest import mock -import mock import pytest import pre_commit.constants as C diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index d806953e9..da48e3323 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -1,7 +1,7 @@ import os.path import sys +from unittest import mock -import mock import pytest import pre_commit.constants as C diff --git a/tests/main_test.py b/tests/main_test.py index 107a2e67d..caccc9a6c 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -1,7 +1,7 @@ import argparse import os.path +from unittest import mock -import mock import pytest import pre_commit.constants as C diff --git a/tests/output_test.py b/tests/output_test.py index 4c641c85e..8b6d450cc 100644 --- a/tests/output_test.py +++ b/tests/output_test.py @@ -1,4 +1,5 @@ -import mock +from unittest import mock + import pytest from pre_commit import color diff --git a/tests/repository_test.py b/tests/repository_test.py index 1f5521b86..43e0362cc 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -2,9 +2,9 @@ import re import shutil import sys +from unittest import mock import cfgv -import mock import pytest import pre_commit.constants as C diff --git a/tests/store_test.py b/tests/store_test.py index 6fc8c0588..bb64feada 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -1,7 +1,7 @@ import os.path import sqlite3 +from unittest import mock -import mock import pytest from pre_commit import git diff --git a/tests/xargs_test.py b/tests/xargs_test.py index c0bbe5238..b999b1ee2 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -2,10 +2,9 @@ import os import sys import time +from unittest import mock -import mock import pytest -import six from pre_commit import parse_shebang from pre_commit import xargs @@ -26,19 +25,10 @@ def test_environ_size(env, expected): @pytest.fixture -def win32_py2_mock(): +def win32_mock(): with mock.patch.object(sys, 'getfilesystemencoding', return_value='utf-8'): with mock.patch.object(sys, 'platform', 'win32'): - with mock.patch.object(six, 'PY2', True): - yield - - -@pytest.fixture -def win32_py3_mock(): - with mock.patch.object(sys, 'getfilesystemencoding', return_value='utf-8'): - with mock.patch.object(sys, 'platform', 'win32'): - with mock.patch.object(six, 'PY2', False): - yield + yield @pytest.fixture @@ -78,7 +68,7 @@ def test_partition_limits(): ) -def test_partition_limit_win32_py3(win32_py3_mock): +def test_partition_limit_win32(win32_mock): cmd = ('ninechars',) # counted as half because of utf-16 encode varargs = ('😑' * 5,) @@ -86,13 +76,6 @@ def test_partition_limit_win32_py3(win32_py3_mock): assert ret == (cmd + varargs,) -def test_partition_limit_win32_py2(win32_py2_mock): - cmd = ('ninechars',) - varargs = ('😑' * 5,) # 4 bytes * 5 - ret = xargs.partition(cmd, varargs, 1, _max_length=31) - assert ret == (cmd + varargs,) - - def test_partition_limit_linux(linux_mock): cmd = ('ninechars',) varargs = ('😑' * 5,) From fa536a86931a4b9c0a7fd590b3b84c3c1ded740a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 10 Jan 2020 19:12:56 -0800 Subject: [PATCH 519/544] mypy passes with check_untyped_defs --- .gitignore | 16 +++++----------- .pre-commit-config.yaml | 5 +++++ pre_commit/color.py | 2 +- pre_commit/color_windows.py | 18 +++++++++++------- pre_commit/commands/autoupdate.py | 4 +++- pre_commit/envcontext.py | 21 +++++++++++++++++---- pre_commit/error_handler.py | 5 +++-- pre_commit/file_lock.py | 14 +++++++++----- pre_commit/languages/all.py | 6 +++++- pre_commit/languages/conda.py | 3 ++- pre_commit/languages/docker.py | 3 ++- pre_commit/languages/python.py | 13 +++---------- pre_commit/languages/ruby.py | 3 ++- pre_commit/languages/rust.py | 4 +++- pre_commit/output.py | 14 ++++++-------- pre_commit/repository.py | 31 +++++++++++++++++++++++++++---- pre_commit/resources/hook-tmpl | 15 ++++++++------- pre_commit/util.py | 1 + pre_commit/xargs.py | 3 ++- setup.cfg | 12 ++++++++++++ tests/languages/all_test.py | 10 +++++----- tests/main_test.py | 14 +++++++++----- tests/parse_shebang_test.py | 24 ++++++++++++++---------- tests/repository_test.py | 6 ++++-- tests/staged_files_only_test.py | 3 ++- 25 files changed, 161 insertions(+), 89 deletions(-) diff --git a/.gitignore b/.gitignore index ae552f4aa..5428b0ad8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,8 @@ *.egg-info -*.iml *.py[co] -.*.sw[a-z] -.coverage -.idea -.project -.pydevproject -.tox -.venv.touch +/.coverage +/.mypy_cache +/.pytest_cache +/.tox +/dist /venv* -coverage-html -dist -.pytest_cache diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aa540e828..e7c441f5c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,6 +42,11 @@ repos: rev: v1.6.0 hooks: - id: setup-cfg-fmt +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.761 + hooks: + - id: mypy + exclude: ^testing/resources/ - repo: meta hooks: - id: check-hooks-apply diff --git a/pre_commit/color.py b/pre_commit/color.py index 667609b40..010342755 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -2,7 +2,7 @@ import sys terminal_supports_color = True -if os.name == 'nt': # pragma: no cover (windows) +if sys.platform == 'win32': # pragma: no cover (windows) from pre_commit.color_windows import enable_virtual_terminal_processing try: enable_virtual_terminal_processing() diff --git a/pre_commit/color_windows.py b/pre_commit/color_windows.py index 3e6e3ca9e..4cbb13413 100644 --- a/pre_commit/color_windows.py +++ b/pre_commit/color_windows.py @@ -1,10 +1,14 @@ -from ctypes import POINTER -from ctypes import windll -from ctypes import WinError -from ctypes import WINFUNCTYPE -from ctypes.wintypes import BOOL -from ctypes.wintypes import DWORD -from ctypes.wintypes import HANDLE +import sys +assert sys.platform == 'win32' + +from ctypes import POINTER # noqa: E402 +from ctypes import windll # noqa: E402 +from ctypes import WinError # noqa: E402 +from ctypes import WINFUNCTYPE # noqa: E402 +from ctypes.wintypes import BOOL # noqa: E402 +from ctypes.wintypes import DWORD # noqa: E402 +from ctypes.wintypes import HANDLE # noqa: E402 + STD_OUTPUT_HANDLE = -11 ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 12e67dce0..def0899a2 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -1,6 +1,8 @@ import collections import os.path import re +from typing import List +from typing import Optional from aspy.yaml import ordered_dump from aspy.yaml import ordered_load @@ -121,7 +123,7 @@ def autoupdate(config_file, store, tags_only, freeze, repos=()): """Auto-update the pre-commit config to the latest versions of repos.""" migrate_config(config_file, quiet=True) retv = 0 - rev_infos = [] + rev_infos: List[Optional[RevInfo]] = [] changed = False config = load_config(config_file) diff --git a/pre_commit/envcontext.py b/pre_commit/envcontext.py index b3f770cc1..d5e5b8037 100644 --- a/pre_commit/envcontext.py +++ b/pre_commit/envcontext.py @@ -1,13 +1,26 @@ -import collections import contextlib +import enum import os +from typing import NamedTuple +from typing import Tuple +from typing import Union -UNSET = collections.namedtuple('UNSET', ())() +class _Unset(enum.Enum): + UNSET = 1 -Var = collections.namedtuple('Var', ('name', 'default')) -Var.__new__.__defaults__ = ('',) +UNSET = _Unset.UNSET + + +class Var(NamedTuple): + name: str + default: str = '' + + +SubstitutionT = Tuple[Union[str, Var], ...] +ValueT = Union[str, _Unset, SubstitutionT] +PatchesT = Tuple[Tuple[str, ValueT], ...] def format_env(parts, env): diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 7f5b76343..5817695f8 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -2,6 +2,7 @@ import os.path import sys import traceback +from typing import Union import pre_commit.constants as C from pre_commit import five @@ -32,8 +33,8 @@ def _log_and_exit(msg, exc, formatted): output.write_line(f'Check the log at {log_path}') with open(log_path, 'wb') as log: - def _log_line(*s): # type: (*str) -> None - output.write_line(*s, stream=log) + def _log_line(s: Union[None, str, bytes] = None) -> None: + output.write_line(s, stream=log) _log_line('### version information') _log_line() diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index cd7ad043e..9aaf93f55 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -1,8 +1,9 @@ import contextlib import errno +import os -try: # pragma: no cover (windows) +if os.name == 'nt': # pragma: no cover (windows) import msvcrt # https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/locking @@ -14,12 +15,14 @@ @contextlib.contextmanager def _locked(fileno, blocked_cb): try: - msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region) + # TODO: https://github.com/python/typeshed/pull/3607 + msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region) # type: ignore except OSError: blocked_cb() while True: try: - msvcrt.locking(fileno, msvcrt.LK_LOCK, _region) + # TODO: https://github.com/python/typeshed/pull/3607 + msvcrt.locking(fileno, msvcrt.LK_LOCK, _region) # type: ignore # noqa: E501 except OSError as e: # Locking violation. Returned when the _LK_LOCK or _LK_RLCK # flag is specified and the file cannot be locked after 10 @@ -37,8 +40,9 @@ def _locked(fileno, blocked_cb): # The documentation however states: # "Regions should be locked only briefly and should be unlocked # before closing a file or exiting the program." - msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region) -except ImportError: # pragma: windows no cover + # TODO: https://github.com/python/typeshed/pull/3607 + msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region) # type: ignore +else: # pramga: windows no cover import fcntl @contextlib.contextmanager diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index bf7bb295f..b25846554 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -1,3 +1,6 @@ +from typing import Any +from typing import Dict + from pre_commit.languages import conda from pre_commit.languages import docker from pre_commit.languages import docker_image @@ -13,6 +16,7 @@ from pre_commit.languages import swift from pre_commit.languages import system + # A language implements the following constant and functions in its module: # # # Use None for no environment @@ -49,7 +53,7 @@ # (returncode, output) # """ -languages = { +languages: Dict[str, Any] = { 'conda': conda, 'docker': docker, 'docker_image': docker_image, diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index fe391c051..d90009cc4 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -2,6 +2,7 @@ import os from pre_commit.envcontext import envcontext +from pre_commit.envcontext import SubstitutionT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var from pre_commit.languages import helpers @@ -18,7 +19,7 @@ def get_env_patch(env): # they can be in $CONDA_PREFIX/bin, $CONDA_PREFIX/Library/bin, # $CONDA_PREFIX/Scripts and $CONDA_PREFIX. Whereas the latter only # seems to be used for python.exe. - path = (os.path.join(env, 'bin'), os.pathsep, Var('PATH')) + path: SubstitutionT = (os.path.join(env, 'bin'), os.pathsep, Var('PATH')) if os.name == 'nt': # pragma: no cover (platform specific) path = (env, os.pathsep) + path path = (os.path.join(env, 'Scripts'), os.pathsep) + path diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index eae9eec97..5a2b65ff7 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -1,5 +1,6 @@ import hashlib import os +from typing import Tuple import pre_commit.constants as C from pre_commit import five @@ -42,7 +43,7 @@ def assert_docker_available(): # pragma: windows no cover def build_docker_image(prefix, **kwargs): # pragma: windows no cover pull = kwargs.pop('pull') assert not kwargs, kwargs - cmd = ( + cmd: Tuple[str, ...] = ( 'docker', 'build', '--tag', docker_tag(prefix), '--label', PRE_COMMIT_LABEL, diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index f7ff3aa2d..96ff976e2 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -1,4 +1,5 @@ import contextlib +import functools import os import sys @@ -64,7 +65,8 @@ def _norm(path): return None -def _get_default_version(): # pragma: no cover (platform dependent) +@functools.lru_cache(maxsize=1) +def get_default_version(): # pragma: no cover (platform dependent) # First attempt from `sys.executable` (or the realpath) exe = _find_by_sys_executable() if exe: @@ -86,15 +88,6 @@ def _get_default_version(): # pragma: no cover (platform dependent) return C.DEFAULT -def get_default_version(): - # TODO: when dropping python2, use `functools.lru_cache(maxsize=1)` - try: - return get_default_version.cached_version - except AttributeError: - get_default_version.cached_version = _get_default_version() - return get_default_version() - - def _sys_executable_matches(version): if version == 'python': return True diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 85d9cedcd..3ac47e981 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -5,6 +5,7 @@ import pre_commit.constants as C from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var from pre_commit.languages import helpers from pre_commit.util import CalledProcessError @@ -18,7 +19,7 @@ def get_env_patch(venv, language_version): # pragma: windows no cover - patches = ( + patches: PatchesT = ( ('GEM_HOME', os.path.join(venv, 'gems')), ('RBENV_ROOT', venv), ('BUNDLE_IGNORE_CONFIG', '1'), diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index de3f6fdd9..0e6e74077 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -1,5 +1,7 @@ import contextlib import os.path +from typing import Set +from typing import Tuple import toml @@ -71,7 +73,7 @@ def install_environment(prefix, version, additional_dependencies): _add_dependencies(prefix.path('Cargo.toml'), lib_deps) with clean_path_on_failure(directory): - packages_to_install = {('--path', '.')} + packages_to_install: Set[Tuple[str, ...]] = {('--path', '.')} for cli_dep in cli_deps: cli_dep = cli_dep[len('cli:'):] package, _, version = cli_dep.partition(':') diff --git a/pre_commit/output.py b/pre_commit/output.py index 6ca0b3785..045999aea 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -1,8 +1,8 @@ +import contextlib import sys from pre_commit import color from pre_commit import five -from pre_commit.util import noop_context def get_hook_message( @@ -71,14 +71,12 @@ def write(s, stream=stdout_byte_stream): def write_line(s=None, stream=stdout_byte_stream, logfile_name=None): - output_streams = [stream] - if logfile_name: - ctx = open(logfile_name, 'ab') - output_streams.append(ctx) - else: - ctx = noop_context() + with contextlib.ExitStack() as exit_stack: + output_streams = [stream] + if logfile_name: + stream = exit_stack.enter_context(open(logfile_name, 'ab')) + output_streams.append(stream) - with ctx: for output_stream in output_streams: if s is not None: output_stream.write(five.to_bytes(s)) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 186f1e4ef..57d6116cb 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -1,8 +1,10 @@ -import collections import json import logging import os import shlex +from typing import NamedTuple +from typing import Sequence +from typing import Set import pre_commit.constants as C from pre_commit import five @@ -49,8 +51,29 @@ def _write_state(prefix, venv, state): _KEYS = tuple(item.key for item in MANIFEST_HOOK_DICT.items) -class Hook(collections.namedtuple('Hook', ('src', 'prefix') + _KEYS)): - __slots__ = () +class Hook(NamedTuple): + src: str + prefix: Prefix + id: str + name: str + entry: str + language: str + alias: str + files: str + exclude: str + types: Sequence[str] + exclude_types: Sequence[str] + additional_dependencies: Sequence[str] + args: Sequence[str] + always_run: bool + pass_filenames: bool + description: str + language_version: str + log_file: str + minimum_pre_commit_version: str + require_serial: bool + stages: Sequence[str] + verbose: bool @property def cmd(self): @@ -201,7 +224,7 @@ def _repository_hooks(repo_config, store, root_config): def install_hook_envs(hooks, store): def _need_installed(): - seen = set() + seen: Set[Hook] = set() ret = [] for hook in hooks: if hook.install_key not in seen and not hook.installed(): diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index e83c126ac..8e6b17b57 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -4,6 +4,7 @@ import distutils.spawn import os import subprocess import sys +from typing import Tuple # work around https://github.com/Homebrew/homebrew-core/issues/30445 os.environ.pop('__PYVENV_LAUNCHER__', None) @@ -12,10 +13,10 @@ HERE = os.path.dirname(os.path.abspath(__file__)) Z40 = '0' * 40 ID_HASH = '138fd403232d2ddd5efb44317e38bf03' # start templated -CONFIG = None -HOOK_TYPE = None -INSTALL_PYTHON = None -SKIP_ON_MISSING_CONFIG = None +CONFIG = '' +HOOK_TYPE = '' +INSTALL_PYTHON = '' +SKIP_ON_MISSING_CONFIG = False # end templated @@ -123,7 +124,7 @@ def _rev_exists(rev): def _pre_push(stdin): remote = sys.argv[1] - opts = () + opts: Tuple[str, ...] = () for line in stdin.decode('UTF-8').splitlines(): _, local_sha, _, remote_sha = line.split() if local_sha == Z40: @@ -146,8 +147,8 @@ def _pre_push(stdin): # pushing the whole tree including root commit opts = ('--all-files',) else: - cmd = ('git', 'rev-parse', f'{first_ancestor}^') - source = subprocess.check_output(cmd).decode().strip() + rev_cmd = ('git', 'rev-parse', f'{first_ancestor}^') + source = subprocess.check_output(rev_cmd).decode().strip() opts = ('--origin', local_sha, '--source', source) if opts: diff --git a/pre_commit/util.py b/pre_commit/util.py index 8c9751b43..cf067cba9 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -152,6 +152,7 @@ def __enter__(self): # tty flags normally change \n to \r\n attrs = termios.tcgetattr(self.r) + assert isinstance(attrs[1], int) attrs[1] &= ~(termios.ONLCR | termios.OPOST) termios.tcsetattr(self.r, termios.TCSANOW, attrs) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index d5d13746c..ed171dc95 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -4,6 +4,7 @@ import os import subprocess import sys +from typing import List from pre_commit import parse_shebang from pre_commit.util import cmd_output_b @@ -56,7 +57,7 @@ def partition(cmd, varargs, target_concurrency, _max_length=None): cmd = tuple(cmd) ret = [] - ret_cmd = [] + ret_cmd: List[str] = [] # Reversed so arguments are in order varargs = list(reversed(varargs)) diff --git a/setup.cfg b/setup.cfg index bf666de68..5126c83ac 100644 --- a/setup.cfg +++ b/setup.cfg @@ -52,3 +52,15 @@ exclude = [bdist_wheel] universal = True + +[mypy] +check_untyped_defs = true +disallow_any_generics = true +disallow_incomplete_defs = true +no_implicit_optional = true + +[mypy-testing.*] +disallow_untyped_defs = false + +[mypy-tests.*] +disallow_untyped_defs = false diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index 5e8c8253a..6f58e2fdf 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -18,7 +18,7 @@ def test_install_environment_argspec(language): expected_argspec = ArgSpec( args=['prefix', 'version', 'additional_dependencies'], ) - argspec = inspect.getfullargpsec(languages[language].install_environment) + argspec = inspect.getfullargspec(languages[language].install_environment) assert argspec == expected_argspec @@ -28,21 +28,21 @@ def test_ENVIRONMENT_DIR(language): @pytest.mark.parametrize('language', all_languages) -def test_run_hook_argpsec(language): +def test_run_hook_argspec(language): expected_argspec = ArgSpec(args=['hook', 'file_args', 'color']) - argspec = inspect.getfullargpsec(languages[language].run_hook) + argspec = inspect.getfullargspec(languages[language].run_hook) assert argspec == expected_argspec @pytest.mark.parametrize('language', all_languages) def test_get_default_version_argspec(language): expected_argspec = ArgSpec(args=[]) - argspec = inspect.getfullargpsec(languages[language].get_default_version) + argspec = inspect.getfullargspec(languages[language].get_default_version) assert argspec == expected_argspec @pytest.mark.parametrize('language', all_languages) def test_healthy_argspec(language): expected_argspec = ArgSpec(args=['prefix', 'language_version']) - argspec = inspect.getfullargpsec(languages[language].healthy) + argspec = inspect.getfullargspec(languages[language].healthy) assert argspec == expected_argspec diff --git a/tests/main_test.py b/tests/main_test.py index caccc9a6c..1ddc7c6c5 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -1,5 +1,8 @@ import argparse import os.path +from typing import NamedTuple +from typing import Optional +from typing import Sequence from unittest import mock import pytest @@ -24,11 +27,11 @@ def test_append_replace_default(argv, expected): assert parser.parse_args(argv).f == expected -class Args: - def __init__(self, **kwargs): - kwargs.setdefault('command', 'help') - kwargs.setdefault('config', C.CONFIG_FILE) - self.__dict__.update(kwargs) +class Args(NamedTuple): + command: str = 'help' + config: str = C.CONFIG_FILE + files: Sequence[str] = [] + repo: Optional[str] = None def test_adjust_args_and_chdir_not_in_git_dir(in_tmpdir): @@ -73,6 +76,7 @@ def test_adjust_args_try_repo_repo_relative(in_git_dir): in_git_dir.join('foo').ensure_dir().chdir() args = Args(command='try-repo', repo='../foo', files=[]) + assert args.repo is not None assert os.path.exists(args.repo) main._adjust_args_and_chdir(args) assert os.getcwd() == in_git_dir diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index 5798c4e24..7a958b010 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -11,6 +11,12 @@ from pre_commit.util import make_executable +def _echo_exe() -> str: + exe = distutils.spawn.find_executable('echo') + assert exe is not None + return exe + + def test_file_doesnt_exist(): assert parse_shebang.parse_filename('herp derp derp') == () @@ -27,8 +33,7 @@ def test_find_executable_full_path(): def test_find_executable_on_path(): - expected = distutils.spawn.find_executable('echo') - assert parse_shebang.find_executable('echo') == expected + assert parse_shebang.find_executable('echo') == _echo_exe() def test_find_executable_not_found_none(): @@ -110,30 +115,29 @@ def test_normexe_already_full_path(): def test_normexe_gives_full_path(): - expected = distutils.spawn.find_executable('echo') - assert parse_shebang.normexe('echo') == expected - assert os.sep in expected + assert parse_shebang.normexe('echo') == _echo_exe() + assert os.sep in _echo_exe() def test_normalize_cmd_trivial(): - cmd = (distutils.spawn.find_executable('echo'), 'hi') + cmd = (_echo_exe(), 'hi') assert parse_shebang.normalize_cmd(cmd) == cmd def test_normalize_cmd_PATH(): cmd = ('echo', '--version') - expected = (distutils.spawn.find_executable('echo'), '--version') + expected = (_echo_exe(), '--version') assert parse_shebang.normalize_cmd(cmd) == expected def test_normalize_cmd_shebang(in_tmpdir): - echo = distutils.spawn.find_executable('echo').replace(os.sep, '/') + echo = _echo_exe().replace(os.sep, '/') path = write_executable(echo) assert parse_shebang.normalize_cmd((path,)) == (echo, path) def test_normalize_cmd_PATH_shebang_full_path(in_tmpdir): - echo = distutils.spawn.find_executable('echo').replace(os.sep, '/') + echo = _echo_exe().replace(os.sep, '/') path = write_executable(echo) with bin_on_path(): ret = parse_shebang.normalize_cmd(('run',)) @@ -141,7 +145,7 @@ def test_normalize_cmd_PATH_shebang_full_path(in_tmpdir): def test_normalize_cmd_PATH_shebang_PATH(in_tmpdir): - echo = distutils.spawn.find_executable('echo') + echo = _echo_exe() path = write_executable('/usr/bin/env echo') with bin_on_path(): ret = parse_shebang.normalize_cmd(('run',)) diff --git a/tests/repository_test.py b/tests/repository_test.py index 43e0362cc..dc4acdc0f 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -2,6 +2,8 @@ import re import shutil import sys +from typing import Any +from typing import Dict from unittest import mock import cfgv @@ -763,7 +765,7 @@ def test_local_python_repo(store, local_python_config): def test_default_language_version(store, local_python_config): - config = { + config: Dict[str, Any] = { 'default_language_version': {'python': 'fake'}, 'default_stages': ['commit'], 'repos': [local_python_config], @@ -780,7 +782,7 @@ def test_default_language_version(store, local_python_config): def test_default_stages(store, local_python_config): - config = { + config: Dict[str, Any] = { 'default_language_version': {'python': C.DEFAULT}, 'default_stages': ['commit'], 'repos': [local_python_config], diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 46e350e18..be9de3953 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -24,7 +24,8 @@ def patch_dir(tempdir_factory): def get_short_git_status(): git_status = cmd_output('git', 'status', '-s')[1] - return dict(reversed(line.split()) for line in git_status.splitlines()) + line_parts = [line.split() for line in git_status.splitlines()] + return {v: k for k, v in line_parts} @pytest.fixture From 327ed924a3c4731f12e974f7d593eb90a7a5938e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 10 Jan 2020 23:32:28 -0800 Subject: [PATCH 520/544] Add types to pre-commit --- .coveragerc | 4 + pre_commit/clientlib.py | 36 +++++--- pre_commit/color.py | 4 +- pre_commit/commands/autoupdate.py | 39 ++++++-- pre_commit/commands/clean.py | 3 +- pre_commit/commands/gc.py | 16 +++- pre_commit/commands/init_templatedir.py | 10 +- pre_commit/commands/install_uninstall.py | 41 ++++++--- pre_commit/commands/migrate_config.py | 13 +-- pre_commit/commands/run.py | 81 ++++++++++++----- pre_commit/commands/sample_config.py | 2 +- pre_commit/commands/try_repo.py | 6 +- pre_commit/envcontext.py | 11 ++- pre_commit/error_handler.py | 12 +-- pre_commit/file_lock.py | 19 +++- pre_commit/five.py | 7 +- pre_commit/git.py | 45 ++++----- pre_commit/languages/conda.py | 28 +++++- pre_commit/languages/docker.py | 38 +++++--- pre_commit/languages/docker_image.py | 12 ++- pre_commit/languages/fail.py | 12 ++- pre_commit/languages/golang.py | 26 +++++- pre_commit/languages/helpers.py | 56 +++++++++--- pre_commit/languages/node.py | 27 ++++-- pre_commit/languages/pygrep.py | 19 +++- pre_commit/languages/python.py | 62 ++++++++++--- pre_commit/languages/python_venv.py | 10 +- pre_commit/languages/ruby.py | 41 +++++++-- pre_commit/languages/rust.py | 32 +++++-- pre_commit/languages/script.py | 12 ++- pre_commit/languages/swift.py | 25 +++-- pre_commit/languages/system.py | 13 ++- pre_commit/logging_handler.py | 10 +- pre_commit/main.py | 26 ++++-- pre_commit/make_archives.py | 7 +- pre_commit/meta_hooks/check_hooks_apply.py | 6 +- .../meta_hooks/check_useless_excludes.py | 12 ++- pre_commit/meta_hooks/identity.py | 5 +- pre_commit/output.py | 41 +++++---- pre_commit/parse_shebang.py | 21 +++-- pre_commit/prefix.py | 13 +-- pre_commit/repository.py | 61 ++++++++----- pre_commit/resources/hook-tmpl | 27 +++--- pre_commit/staged_files_only.py | 9 +- pre_commit/store.py | 68 ++++++++------ pre_commit/util.py | 91 +++++++++++++------ pre_commit/xargs.py | 40 ++++++-- setup.cfg | 1 + tests/color_test.py | 6 +- tests/commands/init_templatedir_test.py | 4 +- tests/conftest.py | 2 +- tests/envcontext_test.py | 4 +- tests/languages/all_test.py | 36 +++++--- tests/languages/docker_test.py | 2 +- tests/languages/helpers_test.py | 6 +- tests/logging_handler_test.py | 16 ++-- tests/main_test.py | 24 ++--- tests/output_test.py | 2 +- tests/repository_test.py | 2 +- tests/store_test.py | 2 +- tests/util_test.py | 8 +- tests/xargs_test.py | 8 +- 62 files changed, 911 insertions(+), 411 deletions(-) diff --git a/.coveragerc b/.coveragerc index d7a248121..14fb527e7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -25,6 +25,10 @@ exclude_lines = ^\s*return NotImplemented\b ^\s*raise$ + # Ignore typing-related things + ^if (False|TYPE_CHECKING): + : \.\.\.$ + # Don't complain if non-runnable code isn't run: ^if __name__ == ['"]__main__['"]:$ diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index c02de282d..d742ef4b3 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -3,6 +3,10 @@ import logging import pipes import sys +from typing import Any +from typing import Dict +from typing import Optional +from typing import Sequence import cfgv from aspy.yaml import ordered_load @@ -18,7 +22,7 @@ check_string_regex = cfgv.check_and(cfgv.check_string, cfgv.check_regex) -def check_type_tag(tag): +def check_type_tag(tag: str) -> None: if tag not in ALL_TAGS: raise cfgv.ValidationError( 'Type tag {!r} is not recognized. ' @@ -26,7 +30,7 @@ def check_type_tag(tag): ) -def check_min_version(version): +def check_min_version(version: str) -> None: if parse_version(version) > parse_version(C.VERSION): raise cfgv.ValidationError( 'pre-commit version {} is required but version {} is installed. ' @@ -36,7 +40,7 @@ def check_min_version(version): ) -def _make_argparser(filenames_help): +def _make_argparser(filenames_help: str) -> argparse.ArgumentParser: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*', help=filenames_help) parser.add_argument('-V', '--version', action='version', version=C.VERSION) @@ -86,7 +90,7 @@ class InvalidManifestError(FatalError): ) -def validate_manifest_main(argv=None): +def validate_manifest_main(argv: Optional[Sequence[str]] = None) -> int: parser = _make_argparser('Manifest filenames.') args = parser.parse_args(argv) ret = 0 @@ -107,7 +111,7 @@ class MigrateShaToRev: key = 'rev' @staticmethod - def _cond(key): + def _cond(key: str) -> cfgv.Conditional: return cfgv.Conditional( key, cfgv.check_string, condition_key='repo', @@ -115,7 +119,7 @@ def _cond(key): ensure_absent=True, ) - def check(self, dct): + def check(self, dct: Dict[str, Any]) -> None: if dct.get('repo') in {LOCAL, META}: self._cond('rev').check(dct) self._cond('sha').check(dct) @@ -126,14 +130,14 @@ def check(self, dct): else: self._cond('rev').check(dct) - def apply_default(self, dct): + def apply_default(self, dct: Dict[str, Any]) -> None: if 'sha' in dct: dct['rev'] = dct.pop('sha') remove_default = cfgv.Required.remove_default -def _entry(modname): +def _entry(modname: str) -> str: """the hook `entry` is passed through `shlex.split()` by the command runner, so to prevent issues with spaces and backslashes (on Windows) it must be quoted here. @@ -143,13 +147,21 @@ def _entry(modname): ) -def warn_unknown_keys_root(extra, orig_keys, dct): +def warn_unknown_keys_root( + extra: Sequence[str], + orig_keys: Sequence[str], + dct: Dict[str, str], +) -> None: logger.warning( 'Unexpected key(s) present at root: {}'.format(', '.join(extra)), ) -def warn_unknown_keys_repo(extra, orig_keys, dct): +def warn_unknown_keys_repo( + extra: Sequence[str], + orig_keys: Sequence[str], + dct: Dict[str, str], +) -> None: logger.warning( 'Unexpected key(s) present on {}: {}'.format( dct['repo'], ', '.join(extra), @@ -281,7 +293,7 @@ class InvalidConfigError(FatalError): pass -def ordered_load_normalize_legacy_config(contents): +def ordered_load_normalize_legacy_config(contents: str) -> Dict[str, Any]: data = ordered_load(contents) if isinstance(data, list): # TODO: Once happy, issue a deprecation warning and instructions @@ -298,7 +310,7 @@ def ordered_load_normalize_legacy_config(contents): ) -def validate_config_main(argv=None): +def validate_config_main(argv: Optional[Sequence[str]] = None) -> int: parser = _make_argparser('Config filenames.') args = parser.parse_args(argv) ret = 0 diff --git a/pre_commit/color.py b/pre_commit/color.py index 010342755..fbb73434f 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -21,7 +21,7 @@ class InvalidColorSetting(ValueError): pass -def format_color(text, color, use_color_setting): +def format_color(text: str, color: str, use_color_setting: bool) -> str: """Format text with color. Args: @@ -38,7 +38,7 @@ def format_color(text, color, use_color_setting): COLOR_CHOICES = ('auto', 'always', 'never') -def use_color(setting): +def use_color(setting: str) -> bool: """Choose whether to use color based on the command argument. Args: diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index def0899a2..2e5ecdf96 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -1,8 +1,12 @@ -import collections import os.path import re +from typing import Any +from typing import Dict from typing import List +from typing import NamedTuple from typing import Optional +from typing import Sequence +from typing import Tuple from aspy.yaml import ordered_dump from aspy.yaml import ordered_load @@ -16,20 +20,23 @@ from pre_commit.clientlib import LOCAL from pre_commit.clientlib import META from pre_commit.commands.migrate_config import migrate_config +from pre_commit.store import Store from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b from pre_commit.util import tmpdir -class RevInfo(collections.namedtuple('RevInfo', ('repo', 'rev', 'frozen'))): - __slots__ = () +class RevInfo(NamedTuple): + repo: str + rev: str + frozen: Optional[str] @classmethod - def from_config(cls, config): + def from_config(cls, config: Dict[str, Any]) -> 'RevInfo': return cls(config['repo'], config['rev'], None) - def update(self, tags_only, freeze): + def update(self, tags_only: bool, freeze: bool) -> 'RevInfo': if tags_only: tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags', '--abbrev=0') else: @@ -57,7 +64,11 @@ class RepositoryCannotBeUpdatedError(RuntimeError): pass -def _check_hooks_still_exist_at_rev(repo_config, info, store): +def _check_hooks_still_exist_at_rev( + repo_config: Dict[str, Any], + info: RevInfo, + store: Store, +) -> None: try: path = store.clone(repo_config['repo'], info.rev) manifest = load_manifest(os.path.join(path, C.MANIFEST_FILE)) @@ -78,7 +89,11 @@ def _check_hooks_still_exist_at_rev(repo_config, info, store): REV_LINE_FMT = '{}rev:{}{}{}{}' -def _original_lines(path, rev_infos, retry=False): +def _original_lines( + path: str, + rev_infos: List[Optional[RevInfo]], + retry: bool = False, +) -> Tuple[List[str], List[int]]: """detect `rev:` lines or reformat the file""" with open(path) as f: original = f.read() @@ -95,7 +110,7 @@ def _original_lines(path, rev_infos, retry=False): return _original_lines(path, rev_infos, retry=True) -def _write_new_config(path, rev_infos): +def _write_new_config(path: str, rev_infos: List[Optional[RevInfo]]) -> None: lines, idxs = _original_lines(path, rev_infos) for idx, rev_info in zip(idxs, rev_infos): @@ -119,7 +134,13 @@ def _write_new_config(path, rev_infos): f.write(''.join(lines)) -def autoupdate(config_file, store, tags_only, freeze, repos=()): +def autoupdate( + config_file: str, + store: Store, + tags_only: bool, + freeze: bool, + repos: Sequence[str] = (), +) -> int: """Auto-update the pre-commit config to the latest versions of repos.""" migrate_config(config_file, quiet=True) retv = 0 diff --git a/pre_commit/commands/clean.py b/pre_commit/commands/clean.py index fe9b40784..2be6c16a5 100644 --- a/pre_commit/commands/clean.py +++ b/pre_commit/commands/clean.py @@ -1,10 +1,11 @@ import os.path from pre_commit import output +from pre_commit.store import Store from pre_commit.util import rmtree -def clean(store): +def clean(store: Store) -> int: legacy_path = os.path.expanduser('~/.pre-commit') for directory in (store.directory, legacy_path): if os.path.exists(directory): diff --git a/pre_commit/commands/gc.py b/pre_commit/commands/gc.py index d35a2c90a..7f6d31119 100644 --- a/pre_commit/commands/gc.py +++ b/pre_commit/commands/gc.py @@ -1,4 +1,8 @@ import os.path +from typing import Any +from typing import Dict +from typing import Set +from typing import Tuple import pre_commit.constants as C from pre_commit import output @@ -8,9 +12,15 @@ from pre_commit.clientlib import load_manifest from pre_commit.clientlib import LOCAL from pre_commit.clientlib import META +from pre_commit.store import Store -def _mark_used_repos(store, all_repos, unused_repos, repo): +def _mark_used_repos( + store: Store, + all_repos: Dict[Tuple[str, str], str], + unused_repos: Set[Tuple[str, str]], + repo: Dict[str, Any], +) -> None: if repo['repo'] == META: return elif repo['repo'] == LOCAL: @@ -47,7 +57,7 @@ def _mark_used_repos(store, all_repos, unused_repos, repo): )) -def _gc_repos(store): +def _gc_repos(store: Store) -> int: configs = store.select_all_configs() repos = store.select_all_repos() @@ -73,7 +83,7 @@ def _gc_repos(store): return len(unused_repos) -def gc(store): +def gc(store: Store) -> int: with store.exclusive_lock(): repos_removed = _gc_repos(store) output.write_line(f'{repos_removed} repo(s) removed.') diff --git a/pre_commit/commands/init_templatedir.py b/pre_commit/commands/init_templatedir.py index 05c902e8e..8ccab55d8 100644 --- a/pre_commit/commands/init_templatedir.py +++ b/pre_commit/commands/init_templatedir.py @@ -1,14 +1,21 @@ import logging import os.path +from typing import Sequence from pre_commit.commands.install_uninstall import install +from pre_commit.store import Store from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output logger = logging.getLogger('pre_commit') -def init_templatedir(config_file, store, directory, hook_types): +def init_templatedir( + config_file: str, + store: Store, + directory: str, + hook_types: Sequence[str], +) -> int: install( config_file, store, hook_types=hook_types, overwrite=True, skip_on_missing_config=True, git_dir=directory, @@ -25,3 +32,4 @@ def init_templatedir(config_file, store, directory, hook_types): logger.warning( f'maybe `git config --global init.templateDir {dest}`?', ) + return 0 diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 6d3a32243..f0e56988f 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -3,12 +3,16 @@ import os.path import shutil import sys +from typing import Optional +from typing import Sequence +from typing import Tuple from pre_commit import git from pre_commit import output from pre_commit.clientlib import load_config from pre_commit.repository import all_hooks from pre_commit.repository import install_hook_envs +from pre_commit.store import Store from pre_commit.util import make_executable from pre_commit.util import mkdirp from pre_commit.util import resource_text @@ -29,13 +33,16 @@ TEMPLATE_END = '# end templated\n' -def _hook_paths(hook_type, git_dir=None): +def _hook_paths( + hook_type: str, + git_dir: Optional[str] = None, +) -> Tuple[str, str]: git_dir = git_dir if git_dir is not None else git.get_git_dir() pth = os.path.join(git_dir, 'hooks', hook_type) return pth, f'{pth}.legacy' -def is_our_script(filename): +def is_our_script(filename: str) -> bool: if not os.path.exists(filename): # pragma: windows no cover (symlink) return False with open(filename) as f: @@ -43,7 +50,7 @@ def is_our_script(filename): return any(h in contents for h in (CURRENT_HASH,) + PRIOR_HASHES) -def shebang(): +def shebang() -> str: if sys.platform == 'win32': py = 'python' else: @@ -63,9 +70,12 @@ def shebang(): def _install_hook_script( - config_file, hook_type, - overwrite=False, skip_on_missing_config=False, git_dir=None, -): + config_file: str, + hook_type: str, + overwrite: bool = False, + skip_on_missing_config: bool = False, + git_dir: Optional[str] = None, +) -> None: hook_path, legacy_path = _hook_paths(hook_type, git_dir=git_dir) mkdirp(os.path.dirname(hook_path)) @@ -108,10 +118,14 @@ def _install_hook_script( def install( - config_file, store, hook_types, - overwrite=False, hooks=False, - skip_on_missing_config=False, git_dir=None, -): + config_file: str, + store: Store, + hook_types: Sequence[str], + overwrite: bool = False, + hooks: bool = False, + skip_on_missing_config: bool = False, + git_dir: Optional[str] = None, +) -> int: if git.has_core_hookpaths_set(): logger.error( 'Cowardly refusing to install hooks with `core.hooksPath` set.\n' @@ -133,11 +147,12 @@ def install( return 0 -def install_hooks(config_file, store): +def install_hooks(config_file: str, store: Store) -> int: install_hook_envs(all_hooks(load_config(config_file), store), store) + return 0 -def _uninstall_hook_script(hook_type): # type: (str) -> None +def _uninstall_hook_script(hook_type: str) -> None: hook_path, legacy_path = _hook_paths(hook_type) # If our file doesn't exist or it isn't ours, gtfo. @@ -152,7 +167,7 @@ def _uninstall_hook_script(hook_type): # type: (str) -> None output.write_line(f'Restored previous hooks to {hook_path}') -def uninstall(hook_types): +def uninstall(hook_types: Sequence[str]) -> int: for hook_type in hook_types: _uninstall_hook_script(hook_type) return 0 diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index 7ea7a6eda..2e3a29fad 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -4,16 +4,16 @@ from aspy.yaml import ordered_load -def _indent(s): +def _indent(s: str) -> str: lines = s.splitlines(True) return ''.join(' ' * 4 + line if line.strip() else line for line in lines) -def _is_header_line(line): - return (line.startswith(('#', '---')) or not line.strip()) +def _is_header_line(line: str) -> bool: + return line.startswith(('#', '---')) or not line.strip() -def _migrate_map(contents): +def _migrate_map(contents: str) -> str: # Find the first non-header line lines = contents.splitlines(True) i = 0 @@ -37,12 +37,12 @@ def _migrate_map(contents): return contents -def _migrate_sha_to_rev(contents): +def _migrate_sha_to_rev(contents: str) -> str: reg = re.compile(r'(\n\s+)sha:') return reg.sub(r'\1rev:', contents) -def migrate_config(config_file, quiet=False): +def migrate_config(config_file: str, quiet: bool = False) -> int: with open(config_file) as f: orig_contents = contents = f.read() @@ -56,3 +56,4 @@ def migrate_config(config_file, quiet=False): print('Configuration has been migrated.') elif not quiet: print('Configuration is already migrated.') + return 0 diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index f56fa9035..c5da7e3c6 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -1,8 +1,17 @@ +import argparse +import functools import logging import os import re import subprocess import time +from typing import Any +from typing import Collection +from typing import Dict +from typing import List +from typing import Sequence +from typing import Set +from typing import Tuple from identify.identify import tags_from_path @@ -12,16 +21,23 @@ from pre_commit.clientlib import load_config from pre_commit.output import get_hook_message from pre_commit.repository import all_hooks +from pre_commit.repository import Hook from pre_commit.repository import install_hook_envs from pre_commit.staged_files_only import staged_files_only +from pre_commit.store import Store from pre_commit.util import cmd_output_b +from pre_commit.util import EnvironT from pre_commit.util import noop_context logger = logging.getLogger('pre_commit') -def filter_by_include_exclude(names, include, exclude): +def filter_by_include_exclude( + names: Collection[str], + include: str, + exclude: str, +) -> List[str]: include_re, exclude_re = re.compile(include), re.compile(exclude) return [ filename for filename in names @@ -31,24 +47,25 @@ def filter_by_include_exclude(names, include, exclude): class Classifier: - def __init__(self, filenames): + def __init__(self, filenames: Sequence[str]) -> None: # on windows we normalize all filenames to use forward slashes # this makes it easier to filter using the `files:` regex # this also makes improperly quoted shell-based hooks work better # see #1173 if os.altsep == '/' and os.sep == '\\': - filenames = (f.replace(os.sep, os.altsep) for f in filenames) + filenames = [f.replace(os.sep, os.altsep) for f in filenames] self.filenames = [f for f in filenames if os.path.lexists(f)] - self._types_cache = {} - def _types_for_file(self, filename): - try: - return self._types_cache[filename] - except KeyError: - ret = self._types_cache[filename] = tags_from_path(filename) - return ret + @functools.lru_cache(maxsize=None) + def _types_for_file(self, filename: str) -> Set[str]: + return tags_from_path(filename) - def by_types(self, names, types, exclude_types): + def by_types( + self, + names: Sequence[str], + types: Collection[str], + exclude_types: Collection[str], + ) -> List[str]: types, exclude_types = frozenset(types), frozenset(exclude_types) ret = [] for filename in names: @@ -57,14 +74,14 @@ def by_types(self, names, types, exclude_types): ret.append(filename) return ret - def filenames_for_hook(self, hook): + def filenames_for_hook(self, hook: Hook) -> Tuple[str, ...]: names = self.filenames names = filter_by_include_exclude(names, hook.files, hook.exclude) names = self.by_types(names, hook.types, hook.exclude_types) - return names + return tuple(names) -def _get_skips(environ): +def _get_skips(environ: EnvironT) -> Set[str]: skips = environ.get('SKIP', '') return {skip.strip() for skip in skips.split(',') if skip.strip()} @@ -73,11 +90,18 @@ def _get_skips(environ): NO_FILES = '(no files to check)' -def _subtle_line(s, use_color): +def _subtle_line(s: str, use_color: bool) -> None: output.write_line(color.format_color(s, color.SUBTLE, use_color)) -def _run_single_hook(classifier, hook, skips, cols, verbose, use_color): +def _run_single_hook( + classifier: Classifier, + hook: Hook, + skips: Set[str], + cols: int, + verbose: bool, + use_color: bool, +) -> bool: filenames = classifier.filenames_for_hook(hook) if hook.id in skips or hook.alias in skips: @@ -115,7 +139,8 @@ def _run_single_hook(classifier, hook, skips, cols, verbose, use_color): diff_cmd = ('git', 'diff', '--no-ext-diff') diff_before = cmd_output_b(*diff_cmd, retcode=None) - filenames = tuple(filenames) if hook.pass_filenames else () + if not hook.pass_filenames: + filenames = () time_before = time.time() retcode, out = hook.run(filenames, use_color) duration = round(time.time() - time_before, 2) or 0 @@ -154,7 +179,7 @@ def _run_single_hook(classifier, hook, skips, cols, verbose, use_color): return files_modified or bool(retcode) -def _compute_cols(hooks): +def _compute_cols(hooks: Sequence[Hook]) -> int: """Compute the number of columns to display hook messages. The widest that will be displayed is in the no files skipped case: @@ -169,7 +194,7 @@ def _compute_cols(hooks): return max(cols, 80) -def _all_filenames(args): +def _all_filenames(args: argparse.Namespace) -> Collection[str]: if args.origin and args.source: return git.get_changed_files(args.origin, args.source) elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}: @@ -184,7 +209,12 @@ def _all_filenames(args): return git.get_staged_files() -def _run_hooks(config, hooks, args, environ): +def _run_hooks( + config: Dict[str, Any], + hooks: Sequence[Hook], + args: argparse.Namespace, + environ: EnvironT, +) -> int: """Actually run the hooks.""" skips = _get_skips(environ) cols = _compute_cols(hooks) @@ -221,12 +251,12 @@ def _run_hooks(config, hooks, args, environ): return retval -def _has_unmerged_paths(): +def _has_unmerged_paths() -> bool: _, stdout, _ = cmd_output_b('git', 'ls-files', '--unmerged') return bool(stdout.strip()) -def _has_unstaged_config(config_file): +def _has_unstaged_config(config_file: str) -> bool: retcode, _, _ = cmd_output_b( 'git', 'diff', '--no-ext-diff', '--exit-code', config_file, retcode=None, @@ -235,7 +265,12 @@ def _has_unstaged_config(config_file): return retcode == 1 -def run(config_file, store, args, environ=os.environ): +def run( + config_file: str, + store: Store, + args: argparse.Namespace, + environ: EnvironT = os.environ, +) -> int: no_stash = args.all_files or bool(args.files) # Check if we have unresolved merge conflict files and fail fast. diff --git a/pre_commit/commands/sample_config.py b/pre_commit/commands/sample_config.py index 60da7cfae..d435faa8c 100644 --- a/pre_commit/commands/sample_config.py +++ b/pre_commit/commands/sample_config.py @@ -16,6 +16,6 @@ ''' -def sample_config(): +def sample_config() -> int: print(SAMPLE_CONFIG, end='') return 0 diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index 061120639..767d2d065 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -1,6 +1,8 @@ +import argparse import collections import logging import os.path +from typing import Tuple from aspy.yaml import ordered_dump @@ -17,7 +19,7 @@ logger = logging.getLogger(__name__) -def _repo_ref(tmpdir, repo, ref): +def _repo_ref(tmpdir: str, repo: str, ref: str) -> Tuple[str, str]: # if `ref` is explicitly passed, use it if ref: return repo, ref @@ -47,7 +49,7 @@ def _repo_ref(tmpdir, repo, ref): return repo, ref -def try_repo(args): +def try_repo(args: argparse.Namespace) -> int: with tmpdir() as tempdir: repo, ref = _repo_ref(tempdir, args.repo, args.ref) diff --git a/pre_commit/envcontext.py b/pre_commit/envcontext.py index d5e5b8037..16d3d15e3 100644 --- a/pre_commit/envcontext.py +++ b/pre_commit/envcontext.py @@ -1,10 +1,14 @@ import contextlib import enum import os +from typing import Generator from typing import NamedTuple +from typing import Optional from typing import Tuple from typing import Union +from pre_commit.util import EnvironT + class _Unset(enum.Enum): UNSET = 1 @@ -23,7 +27,7 @@ class Var(NamedTuple): PatchesT = Tuple[Tuple[str, ValueT], ...] -def format_env(parts, env): +def format_env(parts: SubstitutionT, env: EnvironT) -> str: return ''.join( env.get(part.name, part.default) if isinstance(part, Var) else part for part in parts @@ -31,7 +35,10 @@ def format_env(parts, env): @contextlib.contextmanager -def envcontext(patch, _env=None): +def envcontext( + patch: PatchesT, + _env: Optional[EnvironT] = None, +) -> Generator[None, None, None]: """In this context, `os.environ` is modified according to `patch`. `patch` is an iterable of 2-tuples (key, value): diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 5817695f8..6e67a8903 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -2,6 +2,7 @@ import os.path import sys import traceback +from typing import Generator from typing import Union import pre_commit.constants as C @@ -14,14 +15,11 @@ class FatalError(RuntimeError): pass -def _to_bytes(exc): - try: - return bytes(exc) - except Exception: - return str(exc).encode('UTF-8') +def _to_bytes(exc: BaseException) -> bytes: + return str(exc).encode('UTF-8') -def _log_and_exit(msg, exc, formatted): +def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None: error_msg = b''.join(( five.to_bytes(msg), b': ', five.to_bytes(type(exc).__name__), b': ', @@ -62,7 +60,7 @@ def _log_line(s: Union[None, str, bytes] = None) -> None: @contextlib.contextmanager -def error_handler(): +def error_handler() -> Generator[None, None, None]: try: yield except (Exception, KeyboardInterrupt) as e: diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index 9aaf93f55..241923c7f 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -1,6 +1,8 @@ import contextlib import errno import os +from typing import Callable +from typing import Generator if os.name == 'nt': # pragma: no cover (windows) @@ -13,7 +15,10 @@ _region = 0xffff @contextlib.contextmanager - def _locked(fileno, blocked_cb): + def _locked( + fileno: int, + blocked_cb: Callable[[], None], + ) -> Generator[None, None, None]: try: # TODO: https://github.com/python/typeshed/pull/3607 msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region) # type: ignore @@ -42,11 +47,14 @@ def _locked(fileno, blocked_cb): # before closing a file or exiting the program." # TODO: https://github.com/python/typeshed/pull/3607 msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region) # type: ignore -else: # pramga: windows no cover +else: # pragma: windows no cover import fcntl @contextlib.contextmanager - def _locked(fileno, blocked_cb): + def _locked( + fileno: int, + blocked_cb: Callable[[], None], + ) -> Generator[None, None, None]: try: fcntl.flock(fileno, fcntl.LOCK_EX | fcntl.LOCK_NB) except OSError: # pragma: no cover (tests are single-threaded) @@ -59,7 +67,10 @@ def _locked(fileno, blocked_cb): @contextlib.contextmanager -def lock(path, blocked_cb): +def lock( + path: str, + blocked_cb: Callable[[], None], +) -> Generator[None, None, None]: with open(path, 'a+') as f: with _locked(f.fileno(), blocked_cb): yield diff --git a/pre_commit/five.py b/pre_commit/five.py index 7059b1639..df59d63b0 100644 --- a/pre_commit/five.py +++ b/pre_commit/five.py @@ -1,8 +1,11 @@ -def to_text(s): +from typing import Union + + +def to_text(s: Union[str, bytes]) -> str: return s if isinstance(s, str) else s.decode('UTF-8') -def to_bytes(s): +def to_bytes(s: Union[str, bytes]) -> bytes: return s if isinstance(s, bytes) else s.encode('UTF-8') diff --git a/pre_commit/git.py b/pre_commit/git.py index 4ced8e83f..07be3350a 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -1,15 +1,20 @@ import logging import os.path import sys +from typing import Dict +from typing import List +from typing import Optional +from typing import Set from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b +from pre_commit.util import EnvironT logger = logging.getLogger(__name__) -def zsplit(s): +def zsplit(s: str) -> List[str]: s = s.strip('\0') if s: return s.split('\0') @@ -17,7 +22,7 @@ def zsplit(s): return [] -def no_git_env(_env=None): +def no_git_env(_env: Optional[EnvironT] = None) -> Dict[str, str]: # Too many bugs dealing with environment variables and GIT: # https://github.com/pre-commit/pre-commit/issues/300 # In git 2.6.3 (maybe others), git exports GIT_WORK_TREE while running @@ -34,11 +39,11 @@ def no_git_env(_env=None): } -def get_root(): +def get_root() -> str: return cmd_output('git', 'rev-parse', '--show-toplevel')[1].strip() -def get_git_dir(git_root='.'): +def get_git_dir(git_root: str = '.') -> str: opts = ('--git-common-dir', '--git-dir') _, out, _ = cmd_output('git', 'rev-parse', *opts, cwd=git_root) for line, opt in zip(out.splitlines(), opts): @@ -48,12 +53,12 @@ def get_git_dir(git_root='.'): raise AssertionError('unreachable: no git dir') -def get_remote_url(git_root): +def get_remote_url(git_root: str) -> str: _, out, _ = cmd_output('git', 'config', 'remote.origin.url', cwd=git_root) return out.strip() -def is_in_merge_conflict(): +def is_in_merge_conflict() -> bool: git_dir = get_git_dir('.') return ( os.path.exists(os.path.join(git_dir, 'MERGE_MSG')) and @@ -61,7 +66,7 @@ def is_in_merge_conflict(): ) -def parse_merge_msg_for_conflicts(merge_msg): +def parse_merge_msg_for_conflicts(merge_msg: bytes) -> List[str]: # Conflicted files start with tabs return [ line.lstrip(b'#').strip().decode('UTF-8') @@ -71,7 +76,7 @@ def parse_merge_msg_for_conflicts(merge_msg): ] -def get_conflicted_files(): +def get_conflicted_files() -> Set[str]: logger.info('Checking merge-conflict files only.') # Need to get the conflicted files from the MERGE_MSG because they could # have resolved the conflict by choosing one side or the other @@ -92,7 +97,7 @@ def get_conflicted_files(): return set(merge_conflict_filenames) | set(merge_diff_filenames) -def get_staged_files(cwd=None): +def get_staged_files(cwd: Optional[str] = None) -> List[str]: return zsplit( cmd_output( 'git', 'diff', '--staged', '--name-only', '--no-ext-diff', '-z', @@ -103,7 +108,7 @@ def get_staged_files(cwd=None): ) -def intent_to_add_files(): +def intent_to_add_files() -> List[str]: _, stdout, _ = cmd_output('git', 'status', '--porcelain', '-z') parts = list(reversed(zsplit(stdout))) intent_to_add = [] @@ -117,11 +122,11 @@ def intent_to_add_files(): return intent_to_add -def get_all_files(): +def get_all_files() -> List[str]: return zsplit(cmd_output('git', 'ls-files', '-z')[1]) -def get_changed_files(new, old): +def get_changed_files(new: str, old: str) -> List[str]: return zsplit( cmd_output( 'git', 'diff', '--name-only', '--no-ext-diff', '-z', @@ -130,24 +135,22 @@ def get_changed_files(new, old): ) -def head_rev(remote): +def head_rev(remote: str) -> str: _, out, _ = cmd_output('git', 'ls-remote', '--exit-code', remote, 'HEAD') return out.split()[0] -def has_diff(*args, **kwargs): - repo = kwargs.pop('repo', '.') - assert not kwargs, kwargs +def has_diff(*args: str, repo: str = '.') -> bool: cmd = ('git', 'diff', '--quiet', '--no-ext-diff') + args return cmd_output_b(*cmd, cwd=repo, retcode=None)[0] == 1 -def has_core_hookpaths_set(): +def has_core_hookpaths_set() -> bool: _, out, _ = cmd_output_b('git', 'config', 'core.hooksPath', retcode=None) return bool(out.strip()) -def init_repo(path, remote): +def init_repo(path: str, remote: str) -> None: if os.path.isdir(remote): remote = os.path.abspath(remote) @@ -156,7 +159,7 @@ def init_repo(path, remote): cmd_output_b('git', 'remote', 'add', 'origin', remote, cwd=path, env=env) -def commit(repo='.'): +def commit(repo: str = '.') -> None: env = no_git_env() name, email = 'pre-commit', 'asottile+pre-commit@umich.edu' env['GIT_AUTHOR_NAME'] = env['GIT_COMMITTER_NAME'] = name @@ -165,12 +168,12 @@ def commit(repo='.'): cmd_output_b(*cmd, cwd=repo, env=env) -def git_path(name, repo='.'): +def git_path(name: str, repo: str = '.') -> str: _, out, _ = cmd_output('git', 'rev-parse', '--git-path', name, cwd=repo) return os.path.join(repo, out.strip()) -def check_for_cygwin_mismatch(): +def check_for_cygwin_mismatch() -> None: """See https://github.com/pre-commit/pre-commit/issues/354""" if sys.platform in ('cygwin', 'win32'): # pragma: no cover (windows) is_cygwin_python = sys.platform == 'cygwin' diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index d90009cc4..6c4c786a9 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -1,20 +1,29 @@ import contextlib import os +from typing import Generator +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT from pre_commit.envcontext import SubstitutionT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var from pre_commit.languages import helpers +from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b +if TYPE_CHECKING: + from pre_commit.repository import Hook + ENVIRONMENT_DIR = 'conda' get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy -def get_env_patch(env): +def get_env_patch(env: str) -> PatchesT: # On non-windows systems executable live in $CONDA_PREFIX/bin, on Windows # they can be in $CONDA_PREFIX/bin, $CONDA_PREFIX/Library/bin, # $CONDA_PREFIX/Scripts and $CONDA_PREFIX. Whereas the latter only @@ -34,14 +43,21 @@ def get_env_patch(env): @contextlib.contextmanager -def in_env(prefix, language_version): +def in_env( + prefix: Prefix, + language_version: str, +) -> Generator[None, None, None]: directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version) envdir = prefix.path(directory) with envcontext(get_env_patch(envdir)): yield -def install_environment(prefix, version, additional_dependencies): +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: helpers.assert_version_default('conda', version) directory = helpers.environment_dir(ENVIRONMENT_DIR, version) @@ -58,7 +74,11 @@ def install_environment(prefix, version, additional_dependencies): ) -def run_hook(hook, file_args, color): +def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: # TODO: Some rare commands need to be run using `conda run` but mostly we # can run them withot which is much quicker and produces a better # output. diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 5a2b65ff7..4bef33910 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -1,14 +1,18 @@ import hashlib import os +from typing import Sequence from typing import Tuple +from typing import TYPE_CHECKING import pre_commit.constants as C -from pre_commit import five from pre_commit.languages import helpers +from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b +if TYPE_CHECKING: + from pre_commit.repository import Hook ENVIRONMENT_DIR = 'docker' PRE_COMMIT_LABEL = 'PRE_COMMIT' @@ -16,16 +20,16 @@ healthy = helpers.basic_healthy -def md5(s): # pragma: windows no cover - return hashlib.md5(five.to_bytes(s)).hexdigest() +def md5(s: str) -> str: # pragma: windows no cover + return hashlib.md5(s.encode()).hexdigest() -def docker_tag(prefix): # pragma: windows no cover +def docker_tag(prefix: Prefix) -> str: # pragma: windows no cover md5sum = md5(os.path.basename(prefix.prefix_dir)).lower() return f'pre-commit-{md5sum}' -def docker_is_running(): # pragma: windows no cover +def docker_is_running() -> bool: # pragma: windows no cover try: cmd_output_b('docker', 'ps') except CalledProcessError: @@ -34,15 +38,17 @@ def docker_is_running(): # pragma: windows no cover return True -def assert_docker_available(): # pragma: windows no cover +def assert_docker_available() -> None: # pragma: windows no cover assert docker_is_running(), ( 'Docker is either not running or not configured in this environment' ) -def build_docker_image(prefix, **kwargs): # pragma: windows no cover - pull = kwargs.pop('pull') - assert not kwargs, kwargs +def build_docker_image( + prefix: Prefix, + *, + pull: bool, +) -> None: # pragma: windows no cover cmd: Tuple[str, ...] = ( 'docker', 'build', '--tag', docker_tag(prefix), @@ -56,8 +62,8 @@ def build_docker_image(prefix, **kwargs): # pragma: windows no cover def install_environment( - prefix, version, additional_dependencies, -): # pragma: windows no cover + prefix: Prefix, version: str, additional_dependencies: Sequence[str], +) -> None: # pragma: windows no cover helpers.assert_version_default('docker', version) helpers.assert_no_additional_deps('docker', additional_dependencies) assert_docker_available() @@ -73,14 +79,14 @@ def install_environment( os.mkdir(directory) -def get_docker_user(): # pragma: windows no cover +def get_docker_user() -> str: # pragma: windows no cover try: return '{}:{}'.format(os.getuid(), os.getgid()) except AttributeError: return '1000:1000' -def docker_cmd(): # pragma: windows no cover +def docker_cmd() -> Tuple[str, ...]: # pragma: windows no cover return ( 'docker', 'run', '--rm', @@ -93,7 +99,11 @@ def docker_cmd(): # pragma: windows no cover ) -def run_hook(hook, file_args, color): # pragma: windows no cover +def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: # pragma: windows no cover assert_docker_available() # Rebuild the docker image in case it has gone missing, as many people do # automated cleanup of docker images. diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index 802354011..0bf00e7d8 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -1,7 +1,13 @@ +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING + from pre_commit.languages import helpers from pre_commit.languages.docker import assert_docker_available from pre_commit.languages.docker import docker_cmd +if TYPE_CHECKING: + from pre_commit.repository import Hook ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version @@ -9,7 +15,11 @@ install_environment = helpers.no_install -def run_hook(hook, file_args, color): # pragma: windows no cover +def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: # pragma: windows no cover assert_docker_available() cmd = docker_cmd() + hook.cmd return helpers.run_xargs(hook, cmd, file_args, color=color) diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index 641cbbea4..1ded0713c 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -1,5 +1,11 @@ +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING + from pre_commit.languages import helpers +if TYPE_CHECKING: + from pre_commit.repository import Hook ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version @@ -7,7 +13,11 @@ install_environment = helpers.no_install -def run_hook(hook, file_args, color): +def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: out = hook.entry.encode('UTF-8') + b'\n\n' out += b'\n'.join(f.encode('UTF-8') for f in file_args) + b'\n' return 1, out diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 4f121f248..9d50e6352 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -1,31 +1,39 @@ import contextlib import os.path import sys +from typing import Generator +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING import pre_commit.constants as C from pre_commit import git from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var from pre_commit.languages import helpers +from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b from pre_commit.util import rmtree +if TYPE_CHECKING: + from pre_commit.repository import Hook ENVIRONMENT_DIR = 'golangenv' get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy -def get_env_patch(venv): +def get_env_patch(venv: str) -> PatchesT: return ( ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), ) @contextlib.contextmanager -def in_env(prefix): +def in_env(prefix: Prefix) -> Generator[None, None, None]: envdir = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), ) @@ -33,7 +41,7 @@ def in_env(prefix): yield -def guess_go_dir(remote_url): +def guess_go_dir(remote_url: str) -> str: if remote_url.endswith('.git'): remote_url = remote_url[:-1 * len('.git')] looks_like_url = ( @@ -49,7 +57,11 @@ def guess_go_dir(remote_url): return 'unknown_src_dir' -def install_environment(prefix, version, additional_dependencies): +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: helpers.assert_version_default('golang', version) directory = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), @@ -79,6 +91,10 @@ def install_environment(prefix, version, additional_dependencies): rmtree(pkgdir) -def run_hook(hook, file_args, color): +def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 134a35d05..b39f57aa6 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -1,33 +1,54 @@ import multiprocessing import os import random +from typing import Any +from typing import List +from typing import NoReturn +from typing import Optional +from typing import overload +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING import pre_commit.constants as C +from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b from pre_commit.xargs import xargs +if TYPE_CHECKING: + from pre_commit.repository import Hook + FIXED_RANDOM_SEED = 1542676186 -def run_setup_cmd(prefix, cmd): +def run_setup_cmd(prefix: Prefix, cmd: Tuple[str, ...]) -> None: cmd_output_b(*cmd, cwd=prefix.prefix_dir) -def environment_dir(ENVIRONMENT_DIR, language_version): - if ENVIRONMENT_DIR is None: +@overload +def environment_dir(d: None, language_version: str) -> None: ... +@overload +def environment_dir(d: str, language_version: str) -> str: ... + + +def environment_dir(d: Optional[str], language_version: str) -> Optional[str]: + if d is None: return None else: - return f'{ENVIRONMENT_DIR}-{language_version}' + return f'{d}-{language_version}' -def assert_version_default(binary, version): +def assert_version_default(binary: str, version: str) -> None: if version != C.DEFAULT: raise AssertionError( f'For now, pre-commit requires system-installed {binary}', ) -def assert_no_additional_deps(lang, additional_deps): +def assert_no_additional_deps( + lang: str, + additional_deps: Sequence[str], +) -> None: if additional_deps: raise AssertionError( 'For now, pre-commit does not support ' @@ -35,19 +56,23 @@ def assert_no_additional_deps(lang, additional_deps): ) -def basic_get_default_version(): +def basic_get_default_version() -> str: return C.DEFAULT -def basic_healthy(prefix, language_version): +def basic_healthy(prefix: Prefix, language_version: str) -> bool: return True -def no_install(prefix, version, additional_dependencies): +def no_install( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> NoReturn: raise AssertionError('This type is not installable') -def target_concurrency(hook): +def target_concurrency(hook: 'Hook') -> int: if hook.require_serial or 'PRE_COMMIT_NO_CONCURRENCY' in os.environ: return 1 else: @@ -61,8 +86,8 @@ def target_concurrency(hook): return 1 -def _shuffled(seq): - """Deterministically shuffle identically under both py2 + py3.""" +def _shuffled(seq: Sequence[str]) -> List[str]: + """Deterministically shuffle""" fixed_random = random.Random() fixed_random.seed(FIXED_RANDOM_SEED, version=1) @@ -71,7 +96,12 @@ def _shuffled(seq): return seq -def run_xargs(hook, cmd, file_args, **kwargs): +def run_xargs( + hook: 'Hook', + cmd: Tuple[str, ...], + file_args: Sequence[str], + **kwargs: Any, +) -> Tuple[int, bytes]: # Shuffle the files so that they more evenly fill out the xargs partitions, # but do it deterministically in case a hook cares about ordering. file_args = _shuffled(file_args) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index e0066a265..cb73c12ac 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -1,28 +1,36 @@ import contextlib import os import sys +from typing import Generator +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING import pre_commit.constants as C from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var from pre_commit.languages import helpers from pre_commit.languages.python import bin_dir +from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b +if TYPE_CHECKING: + from pre_commit.repository import Hook ENVIRONMENT_DIR = 'node_env' get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy -def _envdir(prefix, version): +def _envdir(prefix: Prefix, version: str) -> str: directory = helpers.environment_dir(ENVIRONMENT_DIR, version) return prefix.path(directory) -def get_env_patch(venv): # pragma: windows no cover +def get_env_patch(venv: str) -> PatchesT: # pragma: windows no cover if sys.platform == 'cygwin': # pragma: no cover _, win_venv, _ = cmd_output('cygpath', '-w', venv) install_prefix = r'{}\bin'.format(win_venv.strip()) @@ -43,14 +51,17 @@ def get_env_patch(venv): # pragma: windows no cover @contextlib.contextmanager -def in_env(prefix, language_version): # pragma: windows no cover +def in_env( + prefix: Prefix, + language_version: str, +) -> Generator[None, None, None]: # pragma: windows no cover with envcontext(get_env_patch(_envdir(prefix, language_version))): yield def install_environment( - prefix, version, additional_dependencies, -): # pragma: windows no cover + prefix: Prefix, version: str, additional_dependencies: Sequence[str], +) -> None: # pragma: windows no cover additional_dependencies = tuple(additional_dependencies) assert prefix.exists('package.json') envdir = _envdir(prefix, version) @@ -76,6 +87,10 @@ def install_environment( ) -def run_hook(hook, file_args, color): # pragma: windows no cover +def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: # pragma: windows no cover with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 07cfaf128..6b8463d30 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -1,11 +1,18 @@ import argparse import re import sys +from typing import Optional +from typing import Pattern +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING from pre_commit import output from pre_commit.languages import helpers from pre_commit.xargs import xargs +if TYPE_CHECKING: + from pre_commit.repository import Hook ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version @@ -13,7 +20,7 @@ install_environment = helpers.no_install -def _process_filename_by_line(pattern, filename): +def _process_filename_by_line(pattern: Pattern[bytes], filename: str) -> int: retv = 0 with open(filename, 'rb') as f: for line_no, line in enumerate(f, start=1): @@ -24,7 +31,7 @@ def _process_filename_by_line(pattern, filename): return retv -def _process_filename_at_once(pattern, filename): +def _process_filename_at_once(pattern: Pattern[bytes], filename: str) -> int: retv = 0 with open(filename, 'rb') as f: contents = f.read() @@ -41,12 +48,16 @@ def _process_filename_at_once(pattern, filename): return retv -def run_hook(hook, file_args, color): +def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: exe = (sys.executable, '-m', __name__) + tuple(hook.args) + (hook.entry,) return xargs(exe, file_args, color=color) -def main(argv=None): +def main(argv: Optional[Sequence[str]] = None) -> int: parser = argparse.ArgumentParser( description=( 'grep-like finder using python regexes. Unlike grep, this tool ' diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 96ff976e2..3fad9b9b2 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -2,29 +2,40 @@ import functools import os import sys +from typing import Callable +from typing import ContextManager +from typing import Generator +from typing import Optional +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING import pre_commit.constants as C from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var from pre_commit.languages import helpers from pre_commit.parse_shebang import find_executable +from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b +if TYPE_CHECKING: + from pre_commit.repository import Hook ENVIRONMENT_DIR = 'py_env' -def bin_dir(venv): +def bin_dir(venv: str) -> str: """On windows there's a different directory for the virtualenv""" bin_part = 'Scripts' if os.name == 'nt' else 'bin' return os.path.join(venv, bin_part) -def get_env_patch(venv): +def get_env_patch(venv: str) -> PatchesT: return ( ('PYTHONHOME', UNSET), ('VIRTUAL_ENV', venv), @@ -32,7 +43,9 @@ def get_env_patch(venv): ) -def _find_by_py_launcher(version): # pragma: no cover (windows only) +def _find_by_py_launcher( + version: str, +) -> Optional[str]: # pragma: no cover (windows only) if version.startswith('python'): try: return cmd_output( @@ -41,14 +54,16 @@ def _find_by_py_launcher(version): # pragma: no cover (windows only) )[1].strip() except CalledProcessError: pass + return None -def _find_by_sys_executable(): - def _norm(path): +def _find_by_sys_executable() -> Optional[str]: + def _norm(path: str) -> Optional[str]: _, exe = os.path.split(path.lower()) exe, _, _ = exe.partition('.exe') if find_executable(exe) and exe not in {'python', 'pythonw'}: return exe + return None # On linux, I see these common sys.executables: # @@ -66,7 +81,7 @@ def _norm(path): @functools.lru_cache(maxsize=1) -def get_default_version(): # pragma: no cover (platform dependent) +def get_default_version() -> str: # pragma: no cover (platform dependent) # First attempt from `sys.executable` (or the realpath) exe = _find_by_sys_executable() if exe: @@ -88,7 +103,7 @@ def get_default_version(): # pragma: no cover (platform dependent) return C.DEFAULT -def _sys_executable_matches(version): +def _sys_executable_matches(version: str) -> bool: if version == 'python': return True elif not version.startswith('python'): @@ -102,7 +117,7 @@ def _sys_executable_matches(version): return sys.version_info[:len(info)] == info -def norm_version(version): +def norm_version(version: str) -> str: # first see if our current executable is appropriate if _sys_executable_matches(version): return sys.executable @@ -126,14 +141,25 @@ def norm_version(version): return os.path.expanduser(version) -def py_interface(_dir, _make_venv): +def py_interface( + _dir: str, + _make_venv: Callable[[str, str], None], +) -> Tuple[ + Callable[[Prefix, str], ContextManager[None]], + Callable[[Prefix, str], bool], + Callable[['Hook', Sequence[str], bool], Tuple[int, bytes]], + Callable[[Prefix, str, Sequence[str]], None], +]: @contextlib.contextmanager - def in_env(prefix, language_version): + def in_env( + prefix: Prefix, + language_version: str, + ) -> Generator[None, None, None]: envdir = prefix.path(helpers.environment_dir(_dir, language_version)) with envcontext(get_env_patch(envdir)): yield - def healthy(prefix, language_version): + def healthy(prefix: Prefix, language_version: str) -> bool: with in_env(prefix, language_version): retcode, _, _ = cmd_output_b( 'python', '-c', @@ -143,11 +169,19 @@ def healthy(prefix, language_version): ) return retcode == 0 - def run_hook(hook, file_args, color): + def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, + ) -> Tuple[int, bytes]: with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) - def install_environment(prefix, version, additional_dependencies): + def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], + ) -> None: additional_dependencies = tuple(additional_dependencies) directory = helpers.environment_dir(_dir, version) @@ -166,7 +200,7 @@ def install_environment(prefix, version, additional_dependencies): return in_env, healthy, run_hook, install_environment -def make_venv(envdir, python): +def make_venv(envdir: str, python: str) -> None: env = dict(os.environ, VIRTUALENV_NO_DOWNLOAD='1') cmd = (sys.executable, '-mvirtualenv', envdir, '-p', python) cmd_output_b(*cmd, env=env, cwd='/') diff --git a/pre_commit/languages/python_venv.py b/pre_commit/languages/python_venv.py index a1edf9123..5404c8be5 100644 --- a/pre_commit/languages/python_venv.py +++ b/pre_commit/languages/python_venv.py @@ -5,15 +5,11 @@ from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b - ENVIRONMENT_DIR = 'py_venv' +get_default_version = python.get_default_version -def get_default_version(): # pragma: no cover (version specific) - return python.get_default_version() - - -def orig_py_exe(exe): # pragma: no cover (platform specific) +def orig_py_exe(exe: str) -> str: # pragma: no cover (platform specific) """A -mvenv virtualenv made from a -mvirtualenv virtualenv installs packages to the incorrect location. Attempt to find the _original_ exe and invoke `-mvenv` from there. @@ -42,7 +38,7 @@ def orig_py_exe(exe): # pragma: no cover (platform specific) return exe -def make_venv(envdir, python): +def make_venv(envdir: str, python: str) -> None: cmd_output_b(orig_py_exe(python), '-mvenv', envdir, cwd='/') diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 3ac47e981..9f98bea7b 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -2,23 +2,33 @@ import os.path import shutil import tarfile +from typing import Generator +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var from pre_commit.languages import helpers +from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import resource_bytesio +if TYPE_CHECKING: + from pre_comit.repository import Hook ENVIRONMENT_DIR = 'rbenv' get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy -def get_env_patch(venv, language_version): # pragma: windows no cover +def get_env_patch( + venv: str, + language_version: str, +) -> PatchesT: # pragma: windows no cover patches: PatchesT = ( ('GEM_HOME', os.path.join(venv, 'gems')), ('RBENV_ROOT', venv), @@ -36,8 +46,11 @@ def get_env_patch(venv, language_version): # pragma: windows no cover return patches -@contextlib.contextmanager -def in_env(prefix, language_version): # pragma: windows no cover +@contextlib.contextmanager # pragma: windows no cover +def in_env( + prefix: Prefix, + language_version: str, +) -> Generator[None, None, None]: envdir = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, language_version), ) @@ -45,13 +58,16 @@ def in_env(prefix, language_version): # pragma: windows no cover yield -def _extract_resource(filename, dest): +def _extract_resource(filename: str, dest: str) -> None: with resource_bytesio(filename) as bio: with tarfile.open(fileobj=bio) as tf: tf.extractall(dest) -def _install_rbenv(prefix, version=C.DEFAULT): # pragma: windows no cover +def _install_rbenv( + prefix: Prefix, + version: str = C.DEFAULT, +) -> None: # pragma: windows no cover directory = helpers.environment_dir(ENVIRONMENT_DIR, version) _extract_resource('rbenv.tar.gz', prefix.path('.')) @@ -87,7 +103,10 @@ def _install_rbenv(prefix, version=C.DEFAULT): # pragma: windows no cover activate_file.write(f'export RBENV_VERSION="{version}"\n') -def _install_ruby(prefix, version): # pragma: windows no cover +def _install_ruby( + prefix: Prefix, + version: str, +) -> None: # pragma: windows no cover try: helpers.run_setup_cmd(prefix, ('rbenv', 'download', version)) except CalledProcessError: # pragma: no cover (usually find with download) @@ -96,8 +115,8 @@ def _install_ruby(prefix, version): # pragma: windows no cover def install_environment( - prefix, version, additional_dependencies, -): # pragma: windows no cover + prefix: Prefix, version: str, additional_dependencies: Sequence[str], +) -> None: # pragma: windows no cover additional_dependencies = tuple(additional_dependencies) directory = helpers.environment_dir(ENVIRONMENT_DIR, version) with clean_path_on_failure(prefix.path(directory)): @@ -122,6 +141,10 @@ def install_environment( ) -def run_hook(hook, file_args, color): # pragma: windows no cover +def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: # pragma: windows no cover with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 0e6e74077..c570e3c74 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -1,24 +1,31 @@ import contextlib import os.path +from typing import Generator +from typing import Sequence from typing import Set from typing import Tuple +from typing import TYPE_CHECKING import toml import pre_commit.constants as C from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var from pre_commit.languages import helpers +from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b +if TYPE_CHECKING: + from pre_commit.repository import Hook ENVIRONMENT_DIR = 'rustenv' get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy -def get_env_patch(target_dir): +def get_env_patch(target_dir: str) -> PatchesT: return ( ( 'PATH', @@ -28,7 +35,7 @@ def get_env_patch(target_dir): @contextlib.contextmanager -def in_env(prefix): +def in_env(prefix: Prefix) -> Generator[None, None, None]: target_dir = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), ) @@ -36,7 +43,10 @@ def in_env(prefix): yield -def _add_dependencies(cargo_toml_path, additional_dependencies): +def _add_dependencies( + cargo_toml_path: str, + additional_dependencies: Set[str], +) -> None: with open(cargo_toml_path, 'r+') as f: cargo_toml = toml.load(f) cargo_toml.setdefault('dependencies', {}) @@ -48,7 +58,11 @@ def _add_dependencies(cargo_toml_path, additional_dependencies): f.truncate() -def install_environment(prefix, version, additional_dependencies): +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: helpers.assert_version_default('rust', version) directory = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), @@ -82,13 +96,17 @@ def install_environment(prefix, version, additional_dependencies): else: packages_to_install.add((package,)) - for package in packages_to_install: + for args in packages_to_install: cmd_output_b( - 'cargo', 'install', '--bins', '--root', directory, *package, + 'cargo', 'install', '--bins', '--root', directory, *args, cwd=prefix.prefix_dir, ) -def run_hook(hook, file_args, color): +def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index cd5005a9a..2f7235c9d 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -1,5 +1,11 @@ +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING + from pre_commit.languages import helpers +if TYPE_CHECKING: + from pre_commit.repository import Hook ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version @@ -7,7 +13,11 @@ install_environment = helpers.no_install -def run_hook(hook, file_args, color): +def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: cmd = hook.cmd cmd = (hook.prefix.path(cmd[0]),) + cmd[1:] return helpers.run_xargs(hook, cmd, file_args, color=color) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 902d752f2..28e88f374 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -1,13 +1,22 @@ import contextlib import os +from typing import Generator +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING import pre_commit.constants as C from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var from pre_commit.languages import helpers +from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b +if TYPE_CHECKING: + from pre_commit.repository import Hook + ENVIRONMENT_DIR = 'swift_env' get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy @@ -15,13 +24,13 @@ BUILD_CONFIG = 'release' -def get_env_patch(venv): # pragma: windows no cover +def get_env_patch(venv: str) -> PatchesT: # pragma: windows no cover bin_path = os.path.join(venv, BUILD_DIR, BUILD_CONFIG) return (('PATH', (bin_path, os.pathsep, Var('PATH'))),) -@contextlib.contextmanager -def in_env(prefix): # pragma: windows no cover +@contextlib.contextmanager # pragma: windows no cover +def in_env(prefix: Prefix) -> Generator[None, None, None]: envdir = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), ) @@ -30,8 +39,8 @@ def in_env(prefix): # pragma: windows no cover def install_environment( - prefix, version, additional_dependencies, -): # pragma: windows no cover + prefix: Prefix, version: str, additional_dependencies: Sequence[str], +) -> None: # pragma: windows no cover helpers.assert_version_default('swift', version) helpers.assert_no_additional_deps('swift', additional_dependencies) directory = prefix.path( @@ -49,6 +58,10 @@ def install_environment( ) -def run_hook(hook, file_args, color): # pragma: windows no cover +def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: # pragma: windows no cover with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 2d4d6390c..a920f736f 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -1,5 +1,12 @@ +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING + from pre_commit.languages import helpers +if TYPE_CHECKING: + from pre_commit.repository import Hook + ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version @@ -7,5 +14,9 @@ install_environment = helpers.no_install -def run_hook(hook, file_args, color): +def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/logging_handler.py b/pre_commit/logging_handler.py index 0a679a9f5..807b1177d 100644 --- a/pre_commit/logging_handler.py +++ b/pre_commit/logging_handler.py @@ -1,10 +1,10 @@ import contextlib import logging +from typing import Generator from pre_commit import color from pre_commit import output - logger = logging.getLogger('pre_commit') LOG_LEVEL_COLORS = { @@ -16,11 +16,11 @@ class LoggingHandler(logging.Handler): - def __init__(self, use_color): + def __init__(self, use_color: bool) -> None: super().__init__() self.use_color = use_color - def emit(self, record): + def emit(self, record: logging.LogRecord) -> None: output.write_line( '{} {}'.format( color.format_color( @@ -34,8 +34,8 @@ def emit(self, record): @contextlib.contextmanager -def logging_handler(*args, **kwargs): - handler = LoggingHandler(*args, **kwargs) +def logging_handler(use_color: bool) -> Generator[None, None, None]: + handler = LoggingHandler(use_color) logger.addHandler(handler) logger.setLevel(logging.INFO) try: diff --git a/pre_commit/main.py b/pre_commit/main.py index 467d1fbf8..ce902c07e 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -2,6 +2,10 @@ import logging import os import sys +from typing import Any +from typing import Optional +from typing import Sequence +from typing import Union import pre_commit.constants as C from pre_commit import color @@ -37,7 +41,7 @@ COMMANDS_NO_GIT = {'clean', 'gc', 'init-templatedir', 'sample-config'} -def _add_color_option(parser): +def _add_color_option(parser: argparse.ArgumentParser) -> None: parser.add_argument( '--color', default=os.environ.get('PRE_COMMIT_COLOR', 'auto'), type=color.use_color, @@ -46,7 +50,7 @@ def _add_color_option(parser): ) -def _add_config_option(parser): +def _add_config_option(parser: argparse.ArgumentParser) -> None: parser.add_argument( '-c', '--config', default=C.CONFIG_FILE, help='Path to alternate config file', @@ -54,18 +58,24 @@ def _add_config_option(parser): class AppendReplaceDefault(argparse.Action): - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.appended = False - def __call__(self, parser, namespace, values, option_string=None): + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Union[str, Sequence[str], None], + option_string: Optional[str] = None, + ) -> None: if not self.appended: setattr(namespace, self.dest, []) self.appended = True getattr(namespace, self.dest).append(values) -def _add_hook_type_option(parser): +def _add_hook_type_option(parser: argparse.ArgumentParser) -> None: parser.add_argument( '-t', '--hook-type', choices=( 'pre-commit', 'pre-merge-commit', 'pre-push', @@ -77,7 +87,7 @@ def _add_hook_type_option(parser): ) -def _add_run_options(parser): +def _add_run_options(parser: argparse.ArgumentParser) -> None: parser.add_argument('hook', nargs='?', help='A single hook-id to run') parser.add_argument('--verbose', '-v', action='store_true', default=False) parser.add_argument( @@ -111,7 +121,7 @@ def _add_run_options(parser): ) -def _adjust_args_and_chdir(args): +def _adjust_args_and_chdir(args: argparse.Namespace) -> None: # `--config` was specified relative to the non-root working directory if os.path.exists(args.config): args.config = os.path.abspath(args.config) @@ -143,7 +153,7 @@ def _adjust_args_and_chdir(args): args.repo = os.path.relpath(args.repo) -def main(argv=None): +def main(argv: Optional[Sequence[str]] = None) -> int: argv = argv if argv is not None else sys.argv[1:] argv = [five.to_text(arg) for arg in argv] parser = argparse.ArgumentParser(prog='pre-commit') diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index 5a9f81648..5eb1eb7af 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -1,6 +1,8 @@ import argparse import os.path import tarfile +from typing import Optional +from typing import Sequence from pre_commit import output from pre_commit.util import cmd_output_b @@ -23,7 +25,7 @@ ) -def make_archive(name, repo, ref, destdir): +def make_archive(name: str, repo: str, ref: str, destdir: str) -> str: """Makes an archive of a repository in the given destdir. :param text name: Name to give the archive. For instance foo. The file @@ -49,7 +51,7 @@ def make_archive(name, repo, ref, destdir): return output_path -def main(argv=None): +def main(argv: Optional[Sequence[str]] = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('--dest', default='pre_commit/resources') args = parser.parse_args(argv) @@ -58,6 +60,7 @@ def main(argv=None): f'Making {archive_name}.tar.gz for {repo}@{ref}', ) make_archive(archive_name, repo, ref, args.dest) + return 0 if __name__ == '__main__': diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index ef6c9ead5..d0244a944 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -1,4 +1,6 @@ import argparse +from typing import Optional +from typing import Sequence import pre_commit.constants as C from pre_commit import git @@ -8,7 +10,7 @@ from pre_commit.store import Store -def check_all_hooks_match_files(config_file): +def check_all_hooks_match_files(config_file: str) -> int: classifier = Classifier(git.get_all_files()) retv = 0 @@ -22,7 +24,7 @@ def check_all_hooks_match_files(config_file): return retv -def main(argv=None): +def main(argv: Optional[Sequence[str]] = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*', default=[C.CONFIG_FILE]) args = parser.parse_args(argv) diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index f22ff902f..1359e020f 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -1,5 +1,7 @@ import argparse import re +from typing import Optional +from typing import Sequence from cfgv import apply_defaults @@ -10,7 +12,11 @@ from pre_commit.commands.run import Classifier -def exclude_matches_any(filenames, include, exclude): +def exclude_matches_any( + filenames: Sequence[str], + include: str, + exclude: str, +) -> bool: if exclude == '^$': return True include_re, exclude_re = re.compile(include), re.compile(exclude) @@ -20,7 +26,7 @@ def exclude_matches_any(filenames, include, exclude): return False -def check_useless_excludes(config_file): +def check_useless_excludes(config_file: str) -> int: config = load_config(config_file) classifier = Classifier(git.get_all_files()) retv = 0 @@ -52,7 +58,7 @@ def check_useless_excludes(config_file): return retv -def main(argv=None): +def main(argv: Optional[Sequence[str]] = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*', default=[C.CONFIG_FILE]) args = parser.parse_args(argv) diff --git a/pre_commit/meta_hooks/identity.py b/pre_commit/meta_hooks/identity.py index ae7377b80..730d0ec00 100644 --- a/pre_commit/meta_hooks/identity.py +++ b/pre_commit/meta_hooks/identity.py @@ -1,12 +1,15 @@ import sys +from typing import Optional +from typing import Sequence from pre_commit import output -def main(argv=None): +def main(argv: Optional[Sequence[str]] = None) -> int: argv = argv if argv is not None else sys.argv[1:] for arg in argv: output.write_line(arg) + return 0 if __name__ == '__main__': diff --git a/pre_commit/output.py b/pre_commit/output.py index 045999aea..88857ff16 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -1,19 +1,22 @@ import contextlib import sys +from typing import IO +from typing import Optional +from typing import Union from pre_commit import color from pre_commit import five def get_hook_message( - start, - postfix='', - end_msg=None, - end_len=0, - end_color=None, - use_color=None, - cols=80, -): + start: str, + postfix: str = '', + end_msg: Optional[str] = None, + end_len: int = 0, + end_color: Optional[str] = None, + use_color: Optional[bool] = None, + cols: int = 80, +) -> str: """Prints a message for running a hook. This currently supports three approaches: @@ -44,16 +47,13 @@ def get_hook_message( ) start...........................................................postfix end """ - if bool(end_msg) == bool(end_len): - raise ValueError('Expected one of (`end_msg`, `end_len`)') - if end_msg is not None and (end_color is None or use_color is None): - raise ValueError( - '`end_color` and `use_color` are required with `end_msg`', - ) - if end_len: + assert end_msg is None, end_msg return start + '.' * (cols - len(start) - end_len - 1) else: + assert end_msg is not None + assert end_color is not None + assert use_color is not None return '{}{}{}{}\n'.format( start, '.' * (cols - len(start) - len(postfix) - len(end_msg) - 1), @@ -62,15 +62,16 @@ def get_hook_message( ) -stdout_byte_stream = getattr(sys.stdout, 'buffer', sys.stdout) - - -def write(s, stream=stdout_byte_stream): +def write(s: str, stream: IO[bytes] = sys.stdout.buffer) -> None: stream.write(five.to_bytes(s)) stream.flush() -def write_line(s=None, stream=stdout_byte_stream, logfile_name=None): +def write_line( + s: Union[None, str, bytes] = None, + stream: IO[bytes] = sys.stdout.buffer, + logfile_name: Optional[str] = None, +) -> None: with contextlib.ExitStack() as exit_stack: output_streams = [stream] if logfile_name: diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index 8e99bec96..cab90d019 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -1,21 +1,28 @@ import os.path +from typing import Mapping +from typing import NoReturn +from typing import Optional +from typing import Tuple from identify.identify import parse_shebang_from_file class ExecutableNotFoundError(OSError): - def to_output(self): - return (1, self.args[0].encode('UTF-8'), b'') + def to_output(self) -> Tuple[int, bytes, None]: + return (1, self.args[0].encode('UTF-8'), None) -def parse_filename(filename): +def parse_filename(filename: str) -> Tuple[str, ...]: if not os.path.exists(filename): return () else: return parse_shebang_from_file(filename) -def find_executable(exe, _environ=None): +def find_executable( + exe: str, + _environ: Optional[Mapping[str, str]] = None, +) -> Optional[str]: exe = os.path.normpath(exe) if os.sep in exe: return exe @@ -39,8 +46,8 @@ def find_executable(exe, _environ=None): return None -def normexe(orig): - def _error(msg): +def normexe(orig: str) -> str: + def _error(msg: str) -> NoReturn: raise ExecutableNotFoundError(f'Executable `{orig}` {msg}') if os.sep not in orig and (not os.altsep or os.altsep not in orig): @@ -58,7 +65,7 @@ def _error(msg): return orig -def normalize_cmd(cmd): +def normalize_cmd(cmd: Tuple[str, ...]) -> Tuple[str, ...]: """Fixes for the following issues on windows - https://bugs.python.org/issue8557 - windows does not parse shebangs diff --git a/pre_commit/prefix.py b/pre_commit/prefix.py index 17699a3fd..0e3ebbd89 100644 --- a/pre_commit/prefix.py +++ b/pre_commit/prefix.py @@ -1,16 +1,17 @@ -import collections import os.path +from typing import NamedTuple +from typing import Tuple -class Prefix(collections.namedtuple('Prefix', ('prefix_dir',))): - __slots__ = () +class Prefix(NamedTuple): + prefix_dir: str - def path(self, *parts): + def path(self, *parts: str) -> str: return os.path.normpath(os.path.join(self.prefix_dir, *parts)) - def exists(self, *parts): + def exists(self, *parts: str) -> bool: return os.path.exists(self.path(*parts)) - def star(self, end): + def star(self, end: str) -> Tuple[str, ...]: paths = os.listdir(self.prefix_dir) return tuple(path for path in paths if path.endswith(end)) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 57d6116cb..a88566d00 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -2,9 +2,14 @@ import logging import os import shlex +from typing import Any +from typing import Dict +from typing import List from typing import NamedTuple +from typing import Optional from typing import Sequence from typing import Set +from typing import Tuple import pre_commit.constants as C from pre_commit import five @@ -15,6 +20,7 @@ from pre_commit.languages.all import languages from pre_commit.languages.helpers import environment_dir from pre_commit.prefix import Prefix +from pre_commit.store import Store from pre_commit.util import parse_version from pre_commit.util import rmtree @@ -22,15 +28,15 @@ logger = logging.getLogger('pre_commit') -def _state(additional_deps): +def _state(additional_deps: Sequence[str]) -> object: return {'additional_dependencies': sorted(additional_deps)} -def _state_filename(prefix, venv): +def _state_filename(prefix: Prefix, venv: str) -> str: return prefix.path(venv, '.install_state_v' + C.INSTALLED_STATE_VERSION) -def _read_state(prefix, venv): +def _read_state(prefix: Prefix, venv: str) -> Optional[object]: filename = _state_filename(prefix, venv) if not os.path.exists(filename): return None @@ -39,7 +45,7 @@ def _read_state(prefix, venv): return json.load(f) -def _write_state(prefix, venv, state): +def _write_state(prefix: Prefix, venv: str, state: object) -> None: state_filename = _state_filename(prefix, venv) staging = state_filename + 'staging' with open(staging, 'w') as state_file: @@ -76,11 +82,11 @@ class Hook(NamedTuple): verbose: bool @property - def cmd(self): + def cmd(self) -> Tuple[str, ...]: return tuple(shlex.split(self.entry)) + tuple(self.args) @property - def install_key(self): + def install_key(self) -> Tuple[Prefix, str, str, Tuple[str, ...]]: return ( self.prefix, self.language, @@ -88,7 +94,7 @@ def install_key(self): tuple(self.additional_dependencies), ) - def installed(self): + def installed(self) -> bool: lang = languages[self.language] venv = environment_dir(lang.ENVIRONMENT_DIR, self.language_version) return ( @@ -101,7 +107,7 @@ def installed(self): ) ) - def install(self): + def install(self) -> None: logger.info(f'Installing environment for {self.src}.') logger.info('Once installed this environment will be reused.') logger.info('This may take a few minutes...') @@ -120,12 +126,12 @@ def install(self): # Write our state to indicate we're installed _write_state(self.prefix, venv, _state(self.additional_dependencies)) - def run(self, file_args, color): + def run(self, file_args: Sequence[str], color: bool) -> Tuple[int, bytes]: lang = languages[self.language] return lang.run_hook(self, file_args, color) @classmethod - def create(cls, src, prefix, dct): + def create(cls, src: str, prefix: Prefix, dct: Dict[str, Any]) -> 'Hook': # TODO: have cfgv do this (?) extra_keys = set(dct) - set(_KEYS) if extra_keys: @@ -136,9 +142,10 @@ def create(cls, src, prefix, dct): return cls(src=src, prefix=prefix, **{k: dct[k] for k in _KEYS}) -def _hook(*hook_dicts, **kwargs): - root_config = kwargs.pop('root_config') - assert not kwargs, kwargs +def _hook( + *hook_dicts: Dict[str, Any], + root_config: Dict[str, Any], +) -> Dict[str, Any]: ret, rest = dict(hook_dicts[0]), hook_dicts[1:] for dct in rest: ret.update(dct) @@ -166,8 +173,12 @@ def _hook(*hook_dicts, **kwargs): return ret -def _non_cloned_repository_hooks(repo_config, store, root_config): - def _prefix(language_name, deps): +def _non_cloned_repository_hooks( + repo_config: Dict[str, Any], + store: Store, + root_config: Dict[str, Any], +) -> Tuple[Hook, ...]: + def _prefix(language_name: str, deps: Sequence[str]) -> Prefix: language = languages[language_name] # pygrep / script / system / docker_image do not have # environments so they work out of the current directory @@ -186,7 +197,11 @@ def _prefix(language_name, deps): ) -def _cloned_repository_hooks(repo_config, store, root_config): +def _cloned_repository_hooks( + repo_config: Dict[str, Any], + store: Store, + root_config: Dict[str, Any], +) -> Tuple[Hook, ...]: repo, rev = repo_config['repo'], repo_config['rev'] manifest_path = os.path.join(store.clone(repo, rev), C.MANIFEST_FILE) by_id = {hook['id']: hook for hook in load_manifest(manifest_path)} @@ -215,16 +230,20 @@ def _cloned_repository_hooks(repo_config, store, root_config): ) -def _repository_hooks(repo_config, store, root_config): +def _repository_hooks( + repo_config: Dict[str, Any], + store: Store, + root_config: Dict[str, Any], +) -> Tuple[Hook, ...]: if repo_config['repo'] in {LOCAL, META}: return _non_cloned_repository_hooks(repo_config, store, root_config) else: return _cloned_repository_hooks(repo_config, store, root_config) -def install_hook_envs(hooks, store): - def _need_installed(): - seen: Set[Hook] = set() +def install_hook_envs(hooks: Sequence[Hook], store: Store) -> None: + def _need_installed() -> List[Hook]: + seen: Set[Tuple[Prefix, str, str, Tuple[str, ...]]] = set() ret = [] for hook in hooks: if hook.install_key not in seen and not hook.installed(): @@ -240,7 +259,7 @@ def _need_installed(): hook.install() -def all_hooks(root_config, store): +def all_hooks(root_config: Dict[str, Any], store: Store) -> Tuple[Hook, ...]: return tuple( hook for repo in root_config['repos'] diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 8e6b17b57..9bf2af7dc 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -4,6 +4,8 @@ import distutils.spawn import os import subprocess import sys +from typing import Callable +from typing import Dict from typing import Tuple # work around https://github.com/Homebrew/homebrew-core/issues/30445 @@ -28,7 +30,7 @@ class FatalError(RuntimeError): pass -def _norm_exe(exe): +def _norm_exe(exe: str) -> Tuple[str, ...]: """Necessary for shebang support on windows. roughly lifted from `identify.identify.parse_shebang` @@ -47,7 +49,7 @@ def _norm_exe(exe): return tuple(cmd) -def _run_legacy(): +def _run_legacy() -> Tuple[int, bytes]: if __file__.endswith('.legacy'): raise SystemExit( "bug: pre-commit's script is installed in migration mode\n" @@ -59,9 +61,9 @@ def _run_legacy(): ) if HOOK_TYPE == 'pre-push': - stdin = getattr(sys.stdin, 'buffer', sys.stdin).read() + stdin = sys.stdin.buffer.read() else: - stdin = None + stdin = b'' legacy_hook = os.path.join(HERE, f'{HOOK_TYPE}.legacy') if os.access(legacy_hook, os.X_OK): @@ -73,7 +75,7 @@ def _run_legacy(): return 0, stdin -def _validate_config(): +def _validate_config() -> None: cmd = ('git', 'rev-parse', '--show-toplevel') top_level = subprocess.check_output(cmd).decode('UTF-8').strip() cfg = os.path.join(top_level, CONFIG) @@ -97,7 +99,7 @@ def _validate_config(): ) -def _exe(): +def _exe() -> Tuple[str, ...]: with open(os.devnull, 'wb') as devnull: for exe in (INSTALL_PYTHON, sys.executable): try: @@ -117,11 +119,11 @@ def _exe(): ) -def _rev_exists(rev): +def _rev_exists(rev: str) -> bool: return not subprocess.call(('git', 'rev-list', '--quiet', rev)) -def _pre_push(stdin): +def _pre_push(stdin: bytes) -> Tuple[str, ...]: remote = sys.argv[1] opts: Tuple[str, ...] = () @@ -158,8 +160,8 @@ def _pre_push(stdin): raise EarlyExit() -def _opts(stdin): - fns = { +def _opts(stdin: bytes) -> Tuple[str, ...]: + fns: Dict[str, Callable[[bytes], Tuple[str, ...]]] = { 'prepare-commit-msg': lambda _: ('--commit-msg-filename', sys.argv[1]), 'commit-msg': lambda _: ('--commit-msg-filename', sys.argv[1]), 'pre-merge-commit': lambda _: (), @@ -171,13 +173,14 @@ def _opts(stdin): if sys.version_info < (3, 7): # https://bugs.python.org/issue25942 - def _subprocess_call(cmd): # this is the python 2.7 implementation + # this is the python 2.7 implementation + def _subprocess_call(cmd: Tuple[str, ...]) -> int: return subprocess.Popen(cmd).wait() else: _subprocess_call = subprocess.call -def main(): +def main() -> int: retv, stdin = _run_legacy() try: _validate_config() diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index bb81424fd..7f3fff0af 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -2,6 +2,7 @@ import logging import os.path import time +from typing import Generator from pre_commit import git from pre_commit.util import CalledProcessError @@ -14,7 +15,7 @@ logger = logging.getLogger('pre_commit') -def _git_apply(patch): +def _git_apply(patch: str) -> None: args = ('apply', '--whitespace=nowarn', patch) try: cmd_output_b('git', *args) @@ -24,7 +25,7 @@ def _git_apply(patch): @contextlib.contextmanager -def _intent_to_add_cleared(): +def _intent_to_add_cleared() -> Generator[None, None, None]: intent_to_add = git.intent_to_add_files() if intent_to_add: logger.warning('Unstaged intent-to-add files detected.') @@ -39,7 +40,7 @@ def _intent_to_add_cleared(): @contextlib.contextmanager -def _unstaged_changes_cleared(patch_dir): +def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: tree = cmd_output('git', 'write-tree')[1].strip() retcode, diff_stdout_binary, _ = cmd_output_b( 'git', 'diff-index', '--ignore-submodules', '--binary', @@ -84,7 +85,7 @@ def _unstaged_changes_cleared(patch_dir): @contextlib.contextmanager -def staged_files_only(patch_dir): +def staged_files_only(patch_dir: str) -> Generator[None, None, None]: """Clear any unstaged changes from the git working directory inside this context. """ diff --git a/pre_commit/store.py b/pre_commit/store.py index e342e393d..407723c8d 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -3,6 +3,12 @@ import os.path import sqlite3 import tempfile +from typing import Callable +from typing import Generator +from typing import List +from typing import Optional +from typing import Sequence +from typing import Tuple import pre_commit.constants as C from pre_commit import file_lock @@ -18,7 +24,7 @@ logger = logging.getLogger('pre_commit') -def _get_default_directory(): +def _get_default_directory() -> str: """Returns the default directory for the Store. This is intentionally underscored to indicate that `Store.get_default_directory` is the intended way to get this information. This is also done so @@ -34,7 +40,7 @@ def _get_default_directory(): class Store: get_default_directory = staticmethod(_get_default_directory) - def __init__(self, directory=None): + def __init__(self, directory: Optional[str] = None) -> None: self.directory = directory or Store.get_default_directory() self.db_path = os.path.join(self.directory, 'db.db') @@ -66,21 +72,24 @@ def __init__(self, directory=None): ' PRIMARY KEY (repo, ref)' ');', ) - self._create_config_table_if_not_exists(db) + self._create_config_table(db) # Atomic file move os.rename(tmpfile, self.db_path) @contextlib.contextmanager - def exclusive_lock(self): - def blocked_cb(): # pragma: no cover (tests are single-process) + def exclusive_lock(self) -> Generator[None, None, None]: + def blocked_cb() -> None: # pragma: no cover (tests are in-process) logger.info('Locking pre-commit directory') with file_lock.lock(os.path.join(self.directory, '.lock'), blocked_cb): yield @contextlib.contextmanager - def connect(self, db_path=None): + def connect( + self, + db_path: Optional[str] = None, + ) -> Generator[sqlite3.Connection, None, None]: db_path = db_path or self.db_path # sqlite doesn't close its fd with its contextmanager >.< # contextlib.closing fixes this. @@ -91,24 +100,29 @@ def connect(self, db_path=None): yield db @classmethod - def db_repo_name(cls, repo, deps): + def db_repo_name(cls, repo: str, deps: Sequence[str]) -> str: if deps: return '{}:{}'.format(repo, ','.join(sorted(deps))) else: return repo - def _new_repo(self, repo, ref, deps, make_strategy): + def _new_repo( + self, + repo: str, + ref: str, + deps: Sequence[str], + make_strategy: Callable[[str], None], + ) -> str: repo = self.db_repo_name(repo, deps) - def _get_result(): + def _get_result() -> Optional[str]: # Check if we already exist with self.connect() as db: result = db.execute( 'SELECT path FROM repos WHERE repo = ? AND ref = ?', (repo, ref), ).fetchone() - if result: - return result[0] + return result[0] if result else None result = _get_result() if result: @@ -133,14 +147,14 @@ def _get_result(): ) return directory - def _complete_clone(self, ref, git_cmd): + def _complete_clone(self, ref: str, git_cmd: Callable[..., None]) -> None: """Perform a complete clone of a repository and its submodules """ git_cmd('fetch', 'origin', '--tags') git_cmd('checkout', ref) git_cmd('submodule', 'update', '--init', '--recursive') - def _shallow_clone(self, ref, git_cmd): + def _shallow_clone(self, ref: str, git_cmd: Callable[..., None]) -> None: """Perform a shallow clone of a repository and its submodules """ git_config = 'protocol.version=2' @@ -151,14 +165,14 @@ def _shallow_clone(self, ref, git_cmd): '--depth=1', ) - def clone(self, repo, ref, deps=()): + def clone(self, repo: str, ref: str, deps: Sequence[str] = ()) -> str: """Clone the given url and checkout the specific ref.""" - def clone_strategy(directory): + def clone_strategy(directory: str) -> None: git.init_repo(directory, repo) env = git.no_git_env() - def _git_cmd(*args): + def _git_cmd(*args: str) -> None: cmd_output_b('git', *args, cwd=directory, env=env) try: @@ -173,8 +187,8 @@ def _git_cmd(*args): 'pre_commit_dummy_package.gemspec', 'setup.py', 'environment.yml', ) - def make_local(self, deps): - def make_local_strategy(directory): + def make_local(self, deps: Sequence[str]) -> str: + def make_local_strategy(directory: str) -> None: for resource in self.LOCAL_RESOURCES: contents = resource_text(f'empty_template_{resource}') with open(os.path.join(directory, resource), 'w') as f: @@ -183,7 +197,7 @@ def make_local_strategy(directory): env = git.no_git_env() # initialize the git repository so it looks more like cloned repos - def _git_cmd(*args): + def _git_cmd(*args: str) -> None: cmd_output_b('git', *args, cwd=directory, env=env) git.init_repo(directory, '<>') @@ -194,7 +208,7 @@ def _git_cmd(*args): 'local', C.LOCAL_REPO_VERSION, deps, make_local_strategy, ) - def _create_config_table_if_not_exists(self, db): + def _create_config_table(self, db: sqlite3.Connection) -> None: db.executescript( 'CREATE TABLE IF NOT EXISTS configs (' ' path TEXT NOT NULL,' @@ -202,32 +216,32 @@ def _create_config_table_if_not_exists(self, db): ');', ) - def mark_config_used(self, path): + def mark_config_used(self, path: str) -> None: path = os.path.realpath(path) # don't insert config files that do not exist if not os.path.exists(path): return with self.connect() as db: # TODO: eventually remove this and only create in _create - self._create_config_table_if_not_exists(db) + self._create_config_table(db) db.execute('INSERT OR IGNORE INTO configs VALUES (?)', (path,)) - def select_all_configs(self): + def select_all_configs(self) -> List[str]: with self.connect() as db: - self._create_config_table_if_not_exists(db) + self._create_config_table(db) rows = db.execute('SELECT path FROM configs').fetchall() return [path for path, in rows] - def delete_configs(self, configs): + def delete_configs(self, configs: List[str]) -> None: with self.connect() as db: rows = [(path,) for path in configs] db.executemany('DELETE FROM configs WHERE path = ?', rows) - def select_all_repos(self): + def select_all_repos(self) -> List[Tuple[str, str, str]]: with self.connect() as db: return db.execute('SELECT repo, ref, path from repos').fetchall() - def delete_repo(self, db_repo_name, ref, path): + def delete_repo(self, db_repo_name: str, ref: str, path: str) -> None: with self.connect() as db: db.execute( 'DELETE FROM repos WHERE repo = ? and ref = ?', diff --git a/pre_commit/util.py b/pre_commit/util.py index cf067cba9..208ce4970 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -6,6 +6,16 @@ import subprocess import sys import tempfile +from types import TracebackType +from typing import Any +from typing import Callable +from typing import Dict +from typing import Generator +from typing import IO +from typing import Optional +from typing import Tuple +from typing import Type +from typing import Union from pre_commit import five from pre_commit import parse_shebang @@ -17,8 +27,10 @@ from importlib_resources import open_binary from importlib_resources import read_text +EnvironT = Union[Dict[str, str], 'os._Environ'] -def mkdirp(path): + +def mkdirp(path: str) -> None: try: os.makedirs(path) except OSError: @@ -27,7 +39,7 @@ def mkdirp(path): @contextlib.contextmanager -def clean_path_on_failure(path): +def clean_path_on_failure(path: str) -> Generator[None, None, None]: """Cleans up the directory on an exceptional failure.""" try: yield @@ -38,12 +50,12 @@ def clean_path_on_failure(path): @contextlib.contextmanager -def noop_context(): +def noop_context() -> Generator[None, None, None]: yield @contextlib.contextmanager -def tmpdir(): +def tmpdir() -> Generator[str, None, None]: """Contextmanager to create a temporary directory. It will be cleaned up afterwards. """ @@ -54,15 +66,15 @@ def tmpdir(): rmtree(tempdir) -def resource_bytesio(filename): +def resource_bytesio(filename: str) -> IO[bytes]: return open_binary('pre_commit.resources', filename) -def resource_text(filename): +def resource_text(filename: str) -> str: return read_text('pre_commit.resources', filename) -def make_executable(filename): +def make_executable(filename: str) -> None: original_mode = os.stat(filename).st_mode os.chmod( filename, original_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH, @@ -70,18 +82,23 @@ def make_executable(filename): class CalledProcessError(RuntimeError): - def __init__(self, returncode, cmd, expected_returncode, stdout, stderr): - super().__init__( - returncode, cmd, expected_returncode, stdout, stderr, - ) + def __init__( + self, + returncode: int, + cmd: Tuple[str, ...], + expected_returncode: int, + stdout: bytes, + stderr: Optional[bytes], + ) -> None: + super().__init__(returncode, cmd, expected_returncode, stdout, stderr) self.returncode = returncode self.cmd = cmd self.expected_returncode = expected_returncode self.stdout = stdout self.stderr = stderr - def __bytes__(self): - def _indent_or_none(part): + def __bytes__(self) -> bytes: + def _indent_or_none(part: Optional[bytes]) -> bytes: if part: return b'\n ' + part.replace(b'\n', b'\n ') else: @@ -97,11 +114,14 @@ def _indent_or_none(part): b'stderr:', _indent_or_none(self.stderr), )) - def __str__(self): + def __str__(self) -> str: return self.__bytes__().decode('UTF-8') -def _cmd_kwargs(*cmd, **kwargs): +def _cmd_kwargs( + *cmd: str, + **kwargs: Any, +) -> Tuple[Tuple[str, ...], Dict[str, Any]]: # py2/py3 on windows are more strict about the types here cmd = tuple(five.n(arg) for arg in cmd) kwargs['env'] = { @@ -113,7 +133,10 @@ def _cmd_kwargs(*cmd, **kwargs): return cmd, kwargs -def cmd_output_b(*cmd, **kwargs): +def cmd_output_b( + *cmd: str, + **kwargs: Any, +) -> Tuple[int, bytes, Optional[bytes]]: retcode = kwargs.pop('retcode', 0) cmd, kwargs = _cmd_kwargs(*cmd, **kwargs) @@ -132,7 +155,7 @@ def cmd_output_b(*cmd, **kwargs): return returncode, stdout_b, stderr_b -def cmd_output(*cmd, **kwargs): +def cmd_output(*cmd: str, **kwargs: Any) -> Tuple[int, str, Optional[str]]: returncode, stdout_b, stderr_b = cmd_output_b(*cmd, **kwargs) stdout = stdout_b.decode('UTF-8') if stdout_b is not None else None stderr = stderr_b.decode('UTF-8') if stderr_b is not None else None @@ -144,10 +167,11 @@ def cmd_output(*cmd, **kwargs): import termios class Pty: - def __init__(self): - self.r = self.w = None + def __init__(self) -> None: + self.r: Optional[int] = None + self.w: Optional[int] = None - def __enter__(self): + def __enter__(self) -> 'Pty': self.r, self.w = openpty() # tty flags normally change \n to \r\n @@ -158,21 +182,29 @@ def __enter__(self): return self - def close_w(self): + def close_w(self) -> None: if self.w is not None: os.close(self.w) self.w = None - def close_r(self): + def close_r(self) -> None: assert self.r is not None os.close(self.r) self.r = None - def __exit__(self, exc_type, exc_value, traceback): + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: self.close_w() self.close_r() - def cmd_output_p(*cmd, **kwargs): + def cmd_output_p( + *cmd: str, + **kwargs: Any, + ) -> Tuple[int, bytes, Optional[bytes]]: assert kwargs.pop('retcode') is None assert kwargs['stderr'] == subprocess.STDOUT, kwargs['stderr'] cmd, kwargs = _cmd_kwargs(*cmd, **kwargs) @@ -183,6 +215,7 @@ def cmd_output_p(*cmd, **kwargs): return e.to_output() with open(os.devnull) as devnull, Pty() as pty: + assert pty.r is not None kwargs.update({'stdin': devnull, 'stdout': pty.w, 'stderr': pty.w}) proc = subprocess.Popen(cmd, **kwargs) pty.close_w() @@ -206,9 +239,13 @@ def cmd_output_p(*cmd, **kwargs): cmd_output_p = cmd_output_b -def rmtree(path): +def rmtree(path: str) -> None: """On windows, rmtree fails for readonly dirs.""" - def handle_remove_readonly(func, path, exc): + def handle_remove_readonly( + func: Callable[..., Any], + path: str, + exc: Tuple[Type[OSError], OSError, TracebackType], + ) -> None: excvalue = exc[1] if ( func in (os.rmdir, os.remove, os.unlink) and @@ -222,6 +259,6 @@ def handle_remove_readonly(func, path, exc): shutil.rmtree(path, ignore_errors=False, onerror=handle_remove_readonly) -def parse_version(s): +def parse_version(s: str) -> Tuple[int, ...]: """poor man's version comparison""" return tuple(int(p) for p in s.split('.')) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index ed171dc95..ce20d6014 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -4,14 +4,26 @@ import os import subprocess import sys +from typing import Any +from typing import Callable +from typing import Generator +from typing import Iterable from typing import List +from typing import Optional +from typing import Sequence +from typing import Tuple +from typing import TypeVar from pre_commit import parse_shebang from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_p +from pre_commit.util import EnvironT +TArg = TypeVar('TArg') +TRet = TypeVar('TRet') -def _environ_size(_env=None): + +def _environ_size(_env: Optional[EnvironT] = None) -> int: environ = _env if _env is not None else getattr(os, 'environb', os.environ) size = 8 * len(environ) # number of pointers in `envp` for k, v in environ.items(): @@ -19,7 +31,7 @@ def _environ_size(_env=None): return size -def _get_platform_max_length(): # pragma: no cover (platform specific) +def _get_platform_max_length() -> int: # pragma: no cover (platform specific) if os.name == 'posix': maximum = os.sysconf('SC_ARG_MAX') - 2048 - _environ_size() maximum = max(min(maximum, 2 ** 17), 2 ** 12) @@ -31,7 +43,7 @@ def _get_platform_max_length(): # pragma: no cover (platform specific) return 2 ** 12 -def _command_length(*cmd): +def _command_length(*cmd: str) -> int: full_cmd = ' '.join(cmd) # win32 uses the amount of characters, more details at: @@ -47,7 +59,12 @@ class ArgumentTooLongError(RuntimeError): pass -def partition(cmd, varargs, target_concurrency, _max_length=None): +def partition( + cmd: Sequence[str], + varargs: Sequence[str], + target_concurrency: int, + _max_length: Optional[int] = None, +) -> Tuple[Tuple[str, ...], ...]: _max_length = _max_length or _get_platform_max_length() # Generally, we try to partition evenly into at least `target_concurrency` @@ -87,7 +104,10 @@ def partition(cmd, varargs, target_concurrency, _max_length=None): @contextlib.contextmanager -def _thread_mapper(maxsize): +def _thread_mapper(maxsize: int) -> Generator[ + Callable[[Callable[[TArg], TRet], Iterable[TArg]], Iterable[TRet]], + None, None, +]: if maxsize == 1: yield map else: @@ -95,7 +115,11 @@ def _thread_mapper(maxsize): yield ex.map -def xargs(cmd, varargs, **kwargs): +def xargs( + cmd: Tuple[str, ...], + varargs: Sequence[str], + **kwargs: Any, +) -> Tuple[int, bytes]: """A simplified implementation of xargs. color: Make a pty if on a platform that supports it @@ -115,7 +139,9 @@ def xargs(cmd, varargs, **kwargs): partitions = partition(cmd, varargs, target_concurrency, max_length) - def run_cmd_partition(run_cmd): + def run_cmd_partition( + run_cmd: Tuple[str, ...], + ) -> Tuple[int, bytes, Optional[bytes]]: return cmd_fn( *run_cmd, retcode=None, stderr=subprocess.STDOUT, **kwargs, ) diff --git a/setup.cfg b/setup.cfg index 5126c83ac..7dd068650 100644 --- a/setup.cfg +++ b/setup.cfg @@ -57,6 +57,7 @@ universal = True check_untyped_defs = true disallow_any_generics = true disallow_incomplete_defs = true +disallow_untyped_defs = true no_implicit_optional = true [mypy-testing.*] diff --git a/tests/color_test.py b/tests/color_test.py index 4d98bd8d6..50c07d7e0 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -37,21 +37,21 @@ def test_use_color_no_tty(): def test_use_color_tty_with_color_support(): with mock.patch.object(sys.stdout, 'isatty', return_value=True): with mock.patch('pre_commit.color.terminal_supports_color', True): - with envcontext.envcontext([('TERM', envcontext.UNSET)]): + with envcontext.envcontext((('TERM', envcontext.UNSET),)): assert use_color('auto') is True def test_use_color_tty_without_color_support(): with mock.patch.object(sys.stdout, 'isatty', return_value=True): with mock.patch('pre_commit.color.terminal_supports_color', False): - with envcontext.envcontext([('TERM', envcontext.UNSET)]): + with envcontext.envcontext((('TERM', envcontext.UNSET),)): assert use_color('auto') is False def test_use_color_dumb_term(): with mock.patch.object(sys.stdout, 'isatty', return_value=True): with mock.patch('pre_commit.color.terminal_supports_color', True): - with envcontext.envcontext([('TERM', 'dumb')]): + with envcontext.envcontext((('TERM', 'dumb'),)): assert use_color('auto') is False diff --git a/tests/commands/init_templatedir_test.py b/tests/commands/init_templatedir_test.py index 010638d56..4e32e750a 100644 --- a/tests/commands/init_templatedir_test.py +++ b/tests/commands/init_templatedir_test.py @@ -24,7 +24,7 @@ def test_init_templatedir(tmpdir, tempdir_factory, store, cap_out): '[WARNING] maybe `git config --global init.templateDir', ) - with envcontext([('GIT_TEMPLATE_DIR', target)]): + with envcontext((('GIT_TEMPLATE_DIR', target),)): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): @@ -52,7 +52,7 @@ def test_init_templatedir_already_set(tmpdir, tempdir_factory, store, cap_out): def test_init_templatedir_not_set(tmpdir, store, cap_out): # set HOME to ignore the current `.gitconfig` - with envcontext([('HOME', str(tmpdir))]): + with envcontext((('HOME', str(tmpdir)),)): with tmpdir.join('tmpl').ensure_dir().as_cwd(): # we have not set init.templateDir so this should produce a warning init_templatedir( diff --git a/tests/conftest.py b/tests/conftest.py index 6993301e2..21a3034f5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -274,5 +274,5 @@ def fake_log_handler(): @pytest.fixture(scope='session', autouse=True) def set_git_templatedir(tmpdir_factory): tdir = str(tmpdir_factory.mktemp('git_template_dir')) - with envcontext([('GIT_TEMPLATE_DIR', tdir)]): + with envcontext((('GIT_TEMPLATE_DIR', tdir),)): yield diff --git a/tests/envcontext_test.py b/tests/envcontext_test.py index 81f25e381..56dd26328 100644 --- a/tests/envcontext_test.py +++ b/tests/envcontext_test.py @@ -93,7 +93,7 @@ class MyError(RuntimeError): env = {'hello': 'world'} with pytest.raises(MyError): - with envcontext([('foo', 'bar')], _env=env): + with envcontext((('foo', 'bar'),), _env=env): raise MyError() assert env == {'hello': 'world'} @@ -101,6 +101,6 @@ class MyError(RuntimeError): def test_integration_os_environ(): with mock.patch.dict(os.environ, {'FOO': 'bar'}, clear=True): assert os.environ == {'FOO': 'bar'} - with envcontext([('HERP', 'derp')]): + with envcontext((('HERP', 'derp'),)): assert os.environ == {'FOO': 'bar', 'HERP': 'derp'} assert os.environ == {'FOO': 'bar'} diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index 6f58e2fdf..2c3db7cae 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -1,23 +1,31 @@ -import functools import inspect +from typing import Sequence +from typing import Tuple import pytest from pre_commit.languages.all import all_languages from pre_commit.languages.all import languages +from pre_commit.prefix import Prefix -ArgSpec = functools.partial( - inspect.FullArgSpec, varargs=None, varkw=None, defaults=None, - kwonlyargs=[], kwonlydefaults=None, annotations={}, -) +def _argspec(annotations): + args = [k for k in annotations if k != 'return'] + return inspect.FullArgSpec( + args=args, annotations=annotations, + varargs=None, varkw=None, defaults=None, + kwonlyargs=[], kwonlydefaults=None, + ) @pytest.mark.parametrize('language', all_languages) def test_install_environment_argspec(language): - expected_argspec = ArgSpec( - args=['prefix', 'version', 'additional_dependencies'], - ) + expected_argspec = _argspec({ + 'return': None, + 'prefix': Prefix, + 'version': str, + 'additional_dependencies': Sequence[str], + }) argspec = inspect.getfullargspec(languages[language].install_environment) assert argspec == expected_argspec @@ -29,20 +37,26 @@ def test_ENVIRONMENT_DIR(language): @pytest.mark.parametrize('language', all_languages) def test_run_hook_argspec(language): - expected_argspec = ArgSpec(args=['hook', 'file_args', 'color']) + expected_argspec = _argspec({ + 'return': Tuple[int, bytes], + 'hook': 'Hook', 'file_args': Sequence[str], 'color': bool, + }) argspec = inspect.getfullargspec(languages[language].run_hook) assert argspec == expected_argspec @pytest.mark.parametrize('language', all_languages) def test_get_default_version_argspec(language): - expected_argspec = ArgSpec(args=[]) + expected_argspec = _argspec({'return': str}) argspec = inspect.getfullargspec(languages[language].get_default_version) assert argspec == expected_argspec @pytest.mark.parametrize('language', all_languages) def test_healthy_argspec(language): - expected_argspec = ArgSpec(args=['prefix', 'language_version']) + expected_argspec = _argspec({ + 'return': bool, + 'prefix': Prefix, 'language_version': str, + }) argspec = inspect.getfullargspec(languages[language].healthy) assert argspec == expected_argspec diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 9d69a13d9..171a3f732 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -7,7 +7,7 @@ def test_docker_is_running_process_error(): with mock.patch( 'pre_commit.languages.docker.cmd_output_b', - side_effect=CalledProcessError(None, None, None, None, None), + side_effect=CalledProcessError(1, (), 0, b'', None), ): assert docker.docker_is_running() is False diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index b289f7259..c52e947be 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -17,7 +17,7 @@ def test_basic_get_default_version(): def test_basic_healthy(): - assert helpers.basic_healthy(None, None) is True + assert helpers.basic_healthy(Prefix('.'), 'default') is True def test_failed_setup_command_does_not_unicode_error(): @@ -77,4 +77,6 @@ def test_target_concurrency_cpu_count_not_implemented(): def test_shuffled_is_deterministic(): - assert helpers._shuffled(range(10)) == [3, 7, 8, 2, 4, 6, 5, 1, 0, 9] + seq = [str(i) for i in range(10)] + expected = ['3', '7', '8', '2', '4', '6', '5', '1', '0', '9'] + assert helpers._shuffled(seq) == expected diff --git a/tests/logging_handler_test.py b/tests/logging_handler_test.py index 0c2d96f3c..e1506d495 100644 --- a/tests/logging_handler_test.py +++ b/tests/logging_handler_test.py @@ -1,25 +1,21 @@ +import logging + from pre_commit import color from pre_commit.logging_handler import LoggingHandler -class FakeLogRecord: - def __init__(self, message, levelname, levelno): - self.message = message - self.levelname = levelname - self.levelno = levelno - - def getMessage(self): - return self.message +def _log_record(message, level): + return logging.LogRecord('name', level, '', 1, message, {}, None) def test_logging_handler_color(cap_out): handler = LoggingHandler(True) - handler.emit(FakeLogRecord('hi', 'WARNING', 30)) + handler.emit(_log_record('hi', logging.WARNING)) ret = cap_out.get() assert ret == color.YELLOW + '[WARNING]' + color.NORMAL + ' hi\n' def test_logging_handler_no_color(cap_out): handler = LoggingHandler(False) - handler.emit(FakeLogRecord('hi', 'WARNING', 30)) + handler.emit(_log_record('hi', logging.WARNING)) assert cap_out.get() == '[WARNING] hi\n' diff --git a/tests/main_test.py b/tests/main_test.py index 1ddc7c6c5..6a084dca9 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -1,8 +1,5 @@ import argparse import os.path -from typing import NamedTuple -from typing import Optional -from typing import Sequence from unittest import mock import pytest @@ -27,25 +24,24 @@ def test_append_replace_default(argv, expected): assert parser.parse_args(argv).f == expected -class Args(NamedTuple): - command: str = 'help' - config: str = C.CONFIG_FILE - files: Sequence[str] = [] - repo: Optional[str] = None +def _args(**kwargs): + kwargs.setdefault('command', 'help') + kwargs.setdefault('config', C.CONFIG_FILE) + return argparse.Namespace(**kwargs) def test_adjust_args_and_chdir_not_in_git_dir(in_tmpdir): with pytest.raises(FatalError): - main._adjust_args_and_chdir(Args()) + main._adjust_args_and_chdir(_args()) def test_adjust_args_and_chdir_in_dot_git_dir(in_git_dir): with in_git_dir.join('.git').as_cwd(), pytest.raises(FatalError): - main._adjust_args_and_chdir(Args()) + main._adjust_args_and_chdir(_args()) def test_adjust_args_and_chdir_noop(in_git_dir): - args = Args(command='run', files=['f1', 'f2']) + args = _args(command='run', files=['f1', 'f2']) main._adjust_args_and_chdir(args) assert os.getcwd() == in_git_dir assert args.config == C.CONFIG_FILE @@ -56,7 +52,7 @@ def test_adjust_args_and_chdir_relative_things(in_git_dir): in_git_dir.join('foo/cfg.yaml').ensure() in_git_dir.join('foo').chdir() - args = Args(command='run', files=['f1', 'f2'], config='cfg.yaml') + args = _args(command='run', files=['f1', 'f2'], config='cfg.yaml') main._adjust_args_and_chdir(args) assert os.getcwd() == in_git_dir assert args.config == os.path.join('foo', 'cfg.yaml') @@ -66,7 +62,7 @@ def test_adjust_args_and_chdir_relative_things(in_git_dir): def test_adjust_args_and_chdir_non_relative_config(in_git_dir): in_git_dir.join('foo').ensure_dir().chdir() - args = Args() + args = _args() main._adjust_args_and_chdir(args) assert os.getcwd() == in_git_dir assert args.config == C.CONFIG_FILE @@ -75,7 +71,7 @@ def test_adjust_args_and_chdir_non_relative_config(in_git_dir): def test_adjust_args_try_repo_repo_relative(in_git_dir): in_git_dir.join('foo').ensure_dir().chdir() - args = Args(command='try-repo', repo='../foo', files=[]) + args = _args(command='try-repo', repo='../foo', files=[]) assert args.repo is not None assert os.path.exists(args.repo) main._adjust_args_and_chdir(args) diff --git a/tests/output_test.py b/tests/output_test.py index 8b6d450cc..e56c5b74b 100644 --- a/tests/output_test.py +++ b/tests/output_test.py @@ -22,7 +22,7 @@ ), ) def test_get_hook_message_raises(kwargs): - with pytest.raises(ValueError): + with pytest.raises(AssertionError): output.get_hook_message('start', **kwargs) diff --git a/tests/repository_test.py b/tests/repository_test.py index dc4acdc0f..5c541c66a 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -311,7 +311,7 @@ def test_golang_hook(tempdir_factory, store): def test_golang_hook_still_works_when_gobin_is_set(tempdir_factory, store): gobin_dir = tempdir_factory.get() - with envcontext([('GOBIN', gobin_dir)]): + with envcontext((('GOBIN', gobin_dir),)): test_golang_hook(tempdir_factory, store) assert os.listdir(gobin_dir) == [] diff --git a/tests/store_test.py b/tests/store_test.py index bb64feada..586661619 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -120,7 +120,7 @@ def test_clone_shallow_failure_fallback_to_complete( # Force shallow clone failure def fake_shallow_clone(self, *args, **kwargs): - raise CalledProcessError(None, None, None, None, None) + raise CalledProcessError(1, (), 0, b'', None) store._shallow_clone = fake_shallow_clone ret = store.clone(path, rev) diff --git a/tests/util_test.py b/tests/util_test.py index 12373277e..9f75f6a5b 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -15,9 +15,9 @@ def test_CalledProcessError_str(): - error = CalledProcessError(1, ['exe'], 0, b'output', b'errors') + error = CalledProcessError(1, ('exe',), 0, b'output', b'errors') assert str(error) == ( - "command: ['exe']\n" + "command: ('exe',)\n" 'return code: 1\n' 'expected return code: 0\n' 'stdout:\n' @@ -28,9 +28,9 @@ def test_CalledProcessError_str(): def test_CalledProcessError_str_nooutput(): - error = CalledProcessError(1, ['exe'], 0, b'', b'') + error = CalledProcessError(1, ('exe',), 0, b'', b'') assert str(error) == ( - "command: ['exe']\n" + "command: ('exe',)\n" 'return code: 1\n' 'expected return code: 0\n' 'stdout: (none)\n' diff --git a/tests/xargs_test.py b/tests/xargs_test.py index b999b1ee2..1fc920725 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -2,6 +2,7 @@ import os import sys import time +from typing import Tuple from unittest import mock import pytest @@ -166,9 +167,8 @@ def test_xargs_concurrency(): def test_thread_mapper_concurrency_uses_threadpoolexecutor_map(): with xargs._thread_mapper(10) as thread_map: - assert isinstance( - thread_map.__self__, concurrent.futures.ThreadPoolExecutor, - ) is True + _self = thread_map.__self__ # type: ignore + assert isinstance(_self, concurrent.futures.ThreadPoolExecutor) def test_thread_mapper_concurrency_uses_regular_map(): @@ -178,7 +178,7 @@ def test_thread_mapper_concurrency_uses_regular_map(): def test_xargs_propagate_kwargs_to_cmd(): env = {'PRE_COMMIT_TEST_VAR': 'Pre commit is awesome'} - cmd = ('bash', '-c', 'echo $PRE_COMMIT_TEST_VAR', '--') + cmd: Tuple[str, ...] = ('bash', '-c', 'echo $PRE_COMMIT_TEST_VAR', '--') cmd = parse_shebang.normalize_cmd(cmd) ret, stdout = xargs.xargs(cmd, ('1',), env=env) From 4eea90c26c4ddb74bf81a9081e0dad05b82e9d8a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 09:06:06 -0800 Subject: [PATCH 521/544] leverage mypy to check language implementations --- pre_commit/languages/all.py | 93 ++++++++++++++++--------------------- pre_commit/repository.py | 1 + testing/gen-languages-all | 27 +++++++++++ tests/languages/all_test.py | 62 ------------------------- 4 files changed, 69 insertions(+), 114 deletions(-) create mode 100755 testing/gen-languages-all delete mode 100644 tests/languages/all_test.py diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index b25846554..28f44af40 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -1,5 +1,9 @@ -from typing import Any -from typing import Dict +from typing import Callable +from typing import NamedTuple +from typing import Optional +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING from pre_commit.languages import conda from pre_commit.languages import docker @@ -15,58 +19,43 @@ from pre_commit.languages import script from pre_commit.languages import swift from pre_commit.languages import system +from pre_commit.prefix import Prefix +if TYPE_CHECKING: + from pre_commit.repository import Hook -# A language implements the following constant and functions in its module: -# -# # Use None for no environment -# ENVIRONMENT_DIR = 'foo_env' -# -# def get_default_version(): -# """Return a value to replace the 'default' value for language_version. -# -# return 'default' if there is no better option. -# """ -# -# def healthy(prefix, language_version): -# """Return whether or not the environment is considered functional.""" -# -# def install_environment(prefix, version, additional_dependencies): -# """Installs a repository in the given repository. Note that the current -# working directory will already be inside the repository. -# -# Args: -# prefix - `Prefix` bound to the repository. -# version - A version specified in the hook configuration or 'default'. -# """ -# -# def run_hook(hook, file_args, color): -# """Runs a hook and returns the returncode and output of running that -# hook. -# -# Args: -# hook - `Hook` -# file_args - The files to be run -# color - whether the hook should be given a pty (when supported) -# -# Returns: -# (returncode, output) -# """ -languages: Dict[str, Any] = { - 'conda': conda, - 'docker': docker, - 'docker_image': docker_image, - 'fail': fail, - 'golang': golang, - 'node': node, - 'pygrep': pygrep, - 'python': python, - 'python_venv': python_venv, - 'ruby': ruby, - 'rust': rust, - 'script': script, - 'swift': swift, - 'system': system, +class Language(NamedTuple): + name: str + # Use `None` for no installation / environment + ENVIRONMENT_DIR: Optional[str] + # return a value to replace `'default` for `language_version` + get_default_version: Callable[[], str] + # return whether the environment is healthy (or should be rebuilt) + healthy: Callable[[Prefix, str], bool] + # install a repository for the given language and language_version + install_environment: Callable[[Prefix, str, Sequence[str]], None] + # execute a hook and return the exit code and output + run_hook: 'Callable[[Hook, Sequence[str], bool], Tuple[int, bytes]]' + + +# TODO: back to modules + Protocol: https://github.com/python/mypy/issues/5018 +languages = { + # BEGIN GENERATED (testing/gen-languages-all) + 'conda': Language(name='conda', ENVIRONMENT_DIR=conda.ENVIRONMENT_DIR, get_default_version=conda.get_default_version, healthy=conda.healthy, install_environment=conda.install_environment, run_hook=conda.run_hook), # noqa: E501 + 'docker': Language(name='docker', ENVIRONMENT_DIR=docker.ENVIRONMENT_DIR, get_default_version=docker.get_default_version, healthy=docker.healthy, install_environment=docker.install_environment, run_hook=docker.run_hook), # noqa: E501 + 'docker_image': Language(name='docker_image', ENVIRONMENT_DIR=docker_image.ENVIRONMENT_DIR, get_default_version=docker_image.get_default_version, healthy=docker_image.healthy, install_environment=docker_image.install_environment, run_hook=docker_image.run_hook), # noqa: E501 + 'fail': Language(name='fail', ENVIRONMENT_DIR=fail.ENVIRONMENT_DIR, get_default_version=fail.get_default_version, healthy=fail.healthy, install_environment=fail.install_environment, run_hook=fail.run_hook), # noqa: E501 + 'golang': Language(name='golang', ENVIRONMENT_DIR=golang.ENVIRONMENT_DIR, get_default_version=golang.get_default_version, healthy=golang.healthy, install_environment=golang.install_environment, run_hook=golang.run_hook), # noqa: E501 + 'node': Language(name='node', ENVIRONMENT_DIR=node.ENVIRONMENT_DIR, get_default_version=node.get_default_version, healthy=node.healthy, install_environment=node.install_environment, run_hook=node.run_hook), # noqa: E501 + 'pygrep': Language(name='pygrep', ENVIRONMENT_DIR=pygrep.ENVIRONMENT_DIR, get_default_version=pygrep.get_default_version, healthy=pygrep.healthy, install_environment=pygrep.install_environment, run_hook=pygrep.run_hook), # noqa: E501 + 'python': Language(name='python', ENVIRONMENT_DIR=python.ENVIRONMENT_DIR, get_default_version=python.get_default_version, healthy=python.healthy, install_environment=python.install_environment, run_hook=python.run_hook), # noqa: E501 + 'python_venv': Language(name='python_venv', ENVIRONMENT_DIR=python_venv.ENVIRONMENT_DIR, get_default_version=python_venv.get_default_version, healthy=python_venv.healthy, install_environment=python_venv.install_environment, run_hook=python_venv.run_hook), # noqa: E501 + 'ruby': Language(name='ruby', ENVIRONMENT_DIR=ruby.ENVIRONMENT_DIR, get_default_version=ruby.get_default_version, healthy=ruby.healthy, install_environment=ruby.install_environment, run_hook=ruby.run_hook), # noqa: E501 + 'rust': Language(name='rust', ENVIRONMENT_DIR=rust.ENVIRONMENT_DIR, get_default_version=rust.get_default_version, healthy=rust.healthy, install_environment=rust.install_environment, run_hook=rust.run_hook), # noqa: E501 + 'script': Language(name='script', ENVIRONMENT_DIR=script.ENVIRONMENT_DIR, get_default_version=script.get_default_version, healthy=script.healthy, install_environment=script.install_environment, run_hook=script.run_hook), # noqa: E501 + 'swift': Language(name='swift', ENVIRONMENT_DIR=swift.ENVIRONMENT_DIR, get_default_version=swift.get_default_version, healthy=swift.healthy, install_environment=swift.install_environment, run_hook=swift.run_hook), # noqa: E501 + 'system': Language(name='system', ENVIRONMENT_DIR=system.ENVIRONMENT_DIR, get_default_version=system.get_default_version, healthy=system.healthy, install_environment=system.install_environment, run_hook=system.run_hook), # noqa: E501 + # END GENERATED } all_languages = sorted(languages) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index a88566d00..83ed70273 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -113,6 +113,7 @@ def install(self) -> None: logger.info('This may take a few minutes...') lang = languages[self.language] + assert lang.ENVIRONMENT_DIR is not None venv = environment_dir(lang.ENVIRONMENT_DIR, self.language_version) # There's potentially incomplete cleanup from previous runs diff --git a/testing/gen-languages-all b/testing/gen-languages-all new file mode 100755 index 000000000..add6752dc --- /dev/null +++ b/testing/gen-languages-all @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +import sys + +LANGUAGES = [ + 'conda', 'docker', 'docker_image', 'fail', 'golang', 'node', 'pygrep', + 'python', 'python_venv', 'ruby', 'rust', 'script', 'swift', 'system', +] +FIELDS = [ + 'ENVIRONMENT_DIR', 'get_default_version', 'healthy', 'install_environment', + 'run_hook', +] + + +def main() -> int: + print(f' # BEGIN GENERATED ({sys.argv[0]})') + for lang in LANGUAGES: + parts = [f' {lang!r}: Language(name={lang!r}'] + for k in FIELDS: + parts.append(f', {k}={lang}.{k}') + parts.append('), # noqa: E501') + print(''.join(parts)) + print(' # END GENERATED') + return 0 + + +if __name__ == '__main__': + exit(main()) diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py deleted file mode 100644 index 2c3db7cae..000000000 --- a/tests/languages/all_test.py +++ /dev/null @@ -1,62 +0,0 @@ -import inspect -from typing import Sequence -from typing import Tuple - -import pytest - -from pre_commit.languages.all import all_languages -from pre_commit.languages.all import languages -from pre_commit.prefix import Prefix - - -def _argspec(annotations): - args = [k for k in annotations if k != 'return'] - return inspect.FullArgSpec( - args=args, annotations=annotations, - varargs=None, varkw=None, defaults=None, - kwonlyargs=[], kwonlydefaults=None, - ) - - -@pytest.mark.parametrize('language', all_languages) -def test_install_environment_argspec(language): - expected_argspec = _argspec({ - 'return': None, - 'prefix': Prefix, - 'version': str, - 'additional_dependencies': Sequence[str], - }) - argspec = inspect.getfullargspec(languages[language].install_environment) - assert argspec == expected_argspec - - -@pytest.mark.parametrize('language', all_languages) -def test_ENVIRONMENT_DIR(language): - assert hasattr(languages[language], 'ENVIRONMENT_DIR') - - -@pytest.mark.parametrize('language', all_languages) -def test_run_hook_argspec(language): - expected_argspec = _argspec({ - 'return': Tuple[int, bytes], - 'hook': 'Hook', 'file_args': Sequence[str], 'color': bool, - }) - argspec = inspect.getfullargspec(languages[language].run_hook) - assert argspec == expected_argspec - - -@pytest.mark.parametrize('language', all_languages) -def test_get_default_version_argspec(language): - expected_argspec = _argspec({'return': str}) - argspec = inspect.getfullargspec(languages[language].get_default_version) - assert argspec == expected_argspec - - -@pytest.mark.parametrize('language', all_languages) -def test_healthy_argspec(language): - expected_argspec = _argspec({ - 'return': bool, - 'prefix': Prefix, 'language_version': str, - }) - argspec = inspect.getfullargspec(languages[language].healthy) - assert argspec == expected_argspec From 76a184eb07a89903fbe323dd413a9391cff0ac8e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 09:26:44 -0800 Subject: [PATCH 522/544] Update get-swift for bionic --- testing/get-swift.sh | 12 ++++++------ testing/resources/swift_hooks_repo/Package.swift | 4 +++- .../Sources/{ => swift_hooks_repo}/main.swift | 0 3 files changed, 9 insertions(+), 7 deletions(-) rename testing/resources/swift_hooks_repo/Sources/{ => swift_hooks_repo}/main.swift (100%) diff --git a/testing/get-swift.sh b/testing/get-swift.sh index 28986a5f2..e205d44e2 100755 --- a/testing/get-swift.sh +++ b/testing/get-swift.sh @@ -1,14 +1,14 @@ #!/usr/bin/env bash -# This is a script used in travis-ci to install swift +# This is a script used in CI to install swift set -euxo pipefail . /etc/lsb-release -if [ "$DISTRIB_CODENAME" = "trusty" ]; then - SWIFT_URL='https://swift.org/builds/swift-4.0.3-release/ubuntu1404/swift-4.0.3-RELEASE/swift-4.0.3-RELEASE-ubuntu14.04.tar.gz' - SWIFT_HASH="dddb40ec4956e4f6a3f4532d859691d5d1ba8822f6e8b4ec6c452172dbede5ae" +if [ "$DISTRIB_CODENAME" = "bionic" ]; then + SWIFT_URL='https://swift.org/builds/swift-5.1.3-release/ubuntu1804/swift-5.1.3-RELEASE/swift-5.1.3-RELEASE-ubuntu18.04.tar.gz' + SWIFT_HASH='ac82ccd773fe3d586fc340814e31e120da1ff695c6a712f6634e9cc720769610' else - SWIFT_URL='https://swift.org/builds/swift-4.0.3-release/ubuntu1604/swift-4.0.3-RELEASE/swift-4.0.3-RELEASE-ubuntu16.04.tar.gz' - SWIFT_HASH="9adf64cabc7c02ea2d08f150b449b05e46bd42d6e542bf742b3674f5c37f0dbf" + echo "unknown dist: ${DISTRIB_CODENAME}" 1>&2 + exit 1 fi check() { diff --git a/testing/resources/swift_hooks_repo/Package.swift b/testing/resources/swift_hooks_repo/Package.swift index 6e02c188a..04976d3ff 100644 --- a/testing/resources/swift_hooks_repo/Package.swift +++ b/testing/resources/swift_hooks_repo/Package.swift @@ -1,5 +1,7 @@ +// swift-tools-version:5.0 import PackageDescription let package = Package( - name: "swift_hooks_repo" + name: "swift_hooks_repo", + targets: [.target(name: "swift_hooks_repo")] ) diff --git a/testing/resources/swift_hooks_repo/Sources/main.swift b/testing/resources/swift_hooks_repo/Sources/swift_hooks_repo/main.swift similarity index 100% rename from testing/resources/swift_hooks_repo/Sources/main.swift rename to testing/resources/swift_hooks_repo/Sources/swift_hooks_repo/main.swift From aefbe717652ec86a2b5d6099bec8e6b3ff439b77 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 10:46:33 -0800 Subject: [PATCH 523/544] Clean up calls to .encode() / .decode() --- pre_commit/error_handler.py | 2 +- pre_commit/five.py | 4 ++-- pre_commit/git.py | 2 +- pre_commit/languages/fail.py | 4 ++-- pre_commit/parse_shebang.py | 2 +- pre_commit/resources/hook-tmpl | 6 +++--- pre_commit/util.py | 8 ++++---- pre_commit/xargs.py | 1 - testing/resources/arbitrary_bytes_repo/hook.sh | 2 +- tests/conftest.py | 2 +- tests/parse_shebang_test.py | 2 +- 11 files changed, 17 insertions(+), 18 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 6e67a8903..44e19fd41 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -16,7 +16,7 @@ class FatalError(RuntimeError): def _to_bytes(exc: BaseException) -> bytes: - return str(exc).encode('UTF-8') + return str(exc).encode() def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None: diff --git a/pre_commit/five.py b/pre_commit/five.py index df59d63b0..a7ffd9780 100644 --- a/pre_commit/five.py +++ b/pre_commit/five.py @@ -2,11 +2,11 @@ def to_text(s: Union[str, bytes]) -> str: - return s if isinstance(s, str) else s.decode('UTF-8') + return s if isinstance(s, str) else s.decode() def to_bytes(s: Union[str, bytes]) -> bytes: - return s if isinstance(s, bytes) else s.encode('UTF-8') + return s if isinstance(s, bytes) else s.encode() n = to_text diff --git a/pre_commit/git.py b/pre_commit/git.py index 07be3350a..107a3a3a7 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -69,7 +69,7 @@ def is_in_merge_conflict() -> bool: def parse_merge_msg_for_conflicts(merge_msg: bytes) -> List[str]: # Conflicted files start with tabs return [ - line.lstrip(b'#').strip().decode('UTF-8') + line.lstrip(b'#').strip().decode() for line in merge_msg.splitlines() # '#\t' for git 2.4.1 if line.startswith((b'\t', b'#\t')) diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index 1ded0713c..ff495c74c 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -18,6 +18,6 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: - out = hook.entry.encode('UTF-8') + b'\n\n' - out += b'\n'.join(f.encode('UTF-8') for f in file_args) + b'\n' + out = hook.entry.encode() + b'\n\n' + out += b'\n'.join(f.encode() for f in file_args) + b'\n' return 1, out diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index cab90d019..c1264da92 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -9,7 +9,7 @@ class ExecutableNotFoundError(OSError): def to_output(self) -> Tuple[int, bytes, None]: - return (1, self.args[0].encode('UTF-8'), None) + return (1, self.args[0].encode(), None) def parse_filename(filename: str) -> Tuple[str, ...]: diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 9bf2af7dc..68e796902 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -39,7 +39,7 @@ def _norm_exe(exe: str) -> Tuple[str, ...]: if f.read(2) != b'#!': return () try: - first_line = f.readline().decode('UTF-8') + first_line = f.readline().decode() except UnicodeDecodeError: return () @@ -77,7 +77,7 @@ def _run_legacy() -> Tuple[int, bytes]: def _validate_config() -> None: cmd = ('git', 'rev-parse', '--show-toplevel') - top_level = subprocess.check_output(cmd).decode('UTF-8').strip() + top_level = subprocess.check_output(cmd).decode().strip() cfg = os.path.join(top_level, CONFIG) if os.path.isfile(cfg): pass @@ -127,7 +127,7 @@ def _pre_push(stdin: bytes) -> Tuple[str, ...]: remote = sys.argv[1] opts: Tuple[str, ...] = () - for line in stdin.decode('UTF-8').splitlines(): + for line in stdin.decode().splitlines(): _, local_sha, _, remote_sha = line.split() if local_sha == Z40: continue diff --git a/pre_commit/util.py b/pre_commit/util.py index 208ce4970..2b3b5b3ee 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -109,13 +109,13 @@ def _indent_or_none(part: Optional[bytes]) -> bytes: 'return code: {}\n' 'expected return code: {}\n'.format( self.cmd, self.returncode, self.expected_returncode, - ).encode('UTF-8'), + ).encode(), b'stdout:', _indent_or_none(self.stdout), b'\n', b'stderr:', _indent_or_none(self.stderr), )) def __str__(self) -> str: - return self.__bytes__().decode('UTF-8') + return self.__bytes__().decode() def _cmd_kwargs( @@ -157,8 +157,8 @@ def cmd_output_b( def cmd_output(*cmd: str, **kwargs: Any) -> Tuple[int, str, Optional[str]]: returncode, stdout_b, stderr_b = cmd_output_b(*cmd, **kwargs) - stdout = stdout_b.decode('UTF-8') if stdout_b is not None else None - stderr = stderr_b.decode('UTF-8') if stderr_b is not None else None + stdout = stdout_b.decode() if stdout_b is not None else None + stderr = stderr_b.decode() if stderr_b is not None else None return returncode, stdout, stderr diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index ce20d6014..ccd341d49 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -49,7 +49,6 @@ def _command_length(*cmd: str) -> int: # win32 uses the amount of characters, more details at: # https://github.com/pre-commit/pre-commit/pull/839 if sys.platform == 'win32': - # the python2.x apis require bytes, we encode as UTF-8 return len(full_cmd.encode('utf-16le')) // 2 else: return len(full_cmd.encode(sys.getfilesystemencoding())) diff --git a/testing/resources/arbitrary_bytes_repo/hook.sh b/testing/resources/arbitrary_bytes_repo/hook.sh index fb7dbae12..9df0c5a07 100755 --- a/testing/resources/arbitrary_bytes_repo/hook.sh +++ b/testing/resources/arbitrary_bytes_repo/hook.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Intentionally write mixed encoding to the output. This should not crash # pre-commit and should write bytes to the output. -# '☃'.encode('UTF-8') + '²'.encode('latin1') +# '☃'.encode() + '²'.encode('latin1') echo -e '\xe2\x98\x83\xb2' # exit 1 to trigger printing exit 1 diff --git a/tests/conftest.py b/tests/conftest.py index 21a3034f5..8149bb9ae 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -249,7 +249,7 @@ def get_bytes(self): def get(self): """Get the output assuming it was written as UTF-8 bytes""" - return self.get_bytes().decode('UTF-8') + return self.get_bytes().decode() @pytest.fixture diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index 7a958b010..158e57196 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -23,7 +23,7 @@ def test_file_doesnt_exist(): def test_simple_case(tmpdir): x = tmpdir.join('f') - x.write_text('#!/usr/bin/env echo', encoding='UTF-8') + x.write('#!/usr/bin/env echo') make_executable(x.strpath) assert parse_shebang.parse_filename(x.strpath) == ('echo',) From 9000e9dd4102de113cdf33844618b1f0a1eb0e0b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 11:13:39 -0800 Subject: [PATCH 524/544] Some manual .format() -> f-strings --- pre_commit/clientlib.py | 25 +++++------- pre_commit/commands/autoupdate.py | 9 ++--- pre_commit/commands/install_uninstall.py | 6 +-- pre_commit/commands/run.py | 11 +++-- pre_commit/git.py | 16 ++++---- pre_commit/languages/docker.py | 4 +- pre_commit/languages/helpers.py | 4 +- pre_commit/languages/node.py | 2 +- pre_commit/languages/pygrep.py | 2 +- pre_commit/languages/python.py | 12 +++--- pre_commit/logging_handler.py | 14 +++---- .../meta_hooks/check_useless_excludes.py | 7 ++-- pre_commit/output.py | 9 ++--- pre_commit/repository.py | 19 ++++----- pre_commit/resources/hook-tmpl | 30 ++++++-------- pre_commit/staged_files_only.py | 2 +- pre_commit/store.py | 2 +- pre_commit/util.py | 8 ++-- .../stdout_stderr_repo/stdout-stderr-entry | 20 ++++------ .../stdout_stderr_repo/tty-check-entry | 23 +++++------ tests/clientlib_test.py | 12 +++--- tests/commands/autoupdate_test.py | 40 +++++++++---------- tests/commands/install_uninstall_test.py | 4 +- tests/commands/run_test.py | 9 ++--- tests/error_handler_test.py | 4 +- tests/languages/ruby_test.py | 6 +-- tests/repository_test.py | 6 +-- 27 files changed, 133 insertions(+), 173 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index d742ef4b3..46ab3cd05 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -1,7 +1,7 @@ import argparse import functools import logging -import pipes +import shlex import sys from typing import Any from typing import Dict @@ -25,18 +25,17 @@ def check_type_tag(tag: str) -> None: if tag not in ALL_TAGS: raise cfgv.ValidationError( - 'Type tag {!r} is not recognized. ' - 'Try upgrading identify and pre-commit?'.format(tag), + f'Type tag {tag!r} is not recognized. ' + f'Try upgrading identify and pre-commit?', ) def check_min_version(version: str) -> None: if parse_version(version) > parse_version(C.VERSION): raise cfgv.ValidationError( - 'pre-commit version {} is required but version {} is installed. ' - 'Perhaps run `pip install --upgrade pre-commit`.'.format( - version, C.VERSION, - ), + f'pre-commit version {version} is required but version ' + f'{C.VERSION} is installed. ' + f'Perhaps run `pip install --upgrade pre-commit`.', ) @@ -142,9 +141,7 @@ def _entry(modname: str) -> str: runner, so to prevent issues with spaces and backslashes (on Windows) it must be quoted here. """ - return '{} -m pre_commit.meta_hooks.{}'.format( - pipes.quote(sys.executable), modname, - ) + return f'{shlex.quote(sys.executable)} -m pre_commit.meta_hooks.{modname}' def warn_unknown_keys_root( @@ -152,9 +149,7 @@ def warn_unknown_keys_root( orig_keys: Sequence[str], dct: Dict[str, str], ) -> None: - logger.warning( - 'Unexpected key(s) present at root: {}'.format(', '.join(extra)), - ) + logger.warning(f'Unexpected key(s) present at root: {", ".join(extra)}') def warn_unknown_keys_repo( @@ -163,9 +158,7 @@ def warn_unknown_keys_repo( dct: Dict[str, str], ) -> None: logger.warning( - 'Unexpected key(s) present on {}: {}'.format( - dct['repo'], ', '.join(extra), - ), + f'Unexpected key(s) present on {dct["repo"]}: {", ".join(extra)}', ) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 2e5ecdf96..19e82a06a 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -80,13 +80,12 @@ def _check_hooks_still_exist_at_rev( hooks_missing = hooks - {hook['id'] for hook in manifest} if hooks_missing: raise RepositoryCannotBeUpdatedError( - 'Cannot update because the tip of master is missing these hooks:\n' - '{}'.format(', '.join(sorted(hooks_missing))), + f'Cannot update because the tip of HEAD is missing these hooks:\n' + f'{", ".join(sorted(hooks_missing))}', ) REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([^\s#]+)(.*)(\r?\n)$', re.DOTALL) -REV_LINE_FMT = '{}rev:{}{}{}{}' def _original_lines( @@ -126,9 +125,7 @@ def _write_new_config(path: str, rev_infos: List[Optional[RevInfo]]) -> None: comment = '' else: comment = match.group(4) - lines[idx] = REV_LINE_FMT.format( - match.group(1), match.group(2), new_rev, comment, match.group(5), - ) + lines[idx] = f'{match[1]}rev:{match[2]}{new_rev}{comment}{match[5]}' with open(path, 'w') as f: f.write(''.join(lines)) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index f0e56988f..717acb071 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -89,8 +89,8 @@ def _install_hook_script( os.remove(legacy_path) elif os.path.exists(legacy_path): output.write_line( - 'Running in migration mode with existing hooks at {}\n' - 'Use -f to use only pre-commit.'.format(legacy_path), + f'Running in migration mode with existing hooks at {legacy_path}\n' + f'Use -f to use only pre-commit.', ) params = { @@ -110,7 +110,7 @@ def _install_hook_script( hook_file.write(before + TEMPLATE_START) for line in to_template.splitlines(): var = line.split()[0] - hook_file.write('{} = {!r}\n'.format(var, params[var])) + hook_file.write(f'{var} = {params[var]!r}\n') hook_file.write(TEMPLATE_END + after) make_executable(hook_path) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index c5da7e3c6..1b08df913 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -243,9 +243,10 @@ def _run_hooks( output.write_line('All changes made by hooks:') # args.color is a boolean. # See user_color function in color.py + git_color_opt = 'always' if args.color else 'never' subprocess.call(( 'git', '--no-pager', 'diff', '--no-ext-diff', - '--color={}'.format({True: 'always', False: 'never'}[args.color]), + f'--color={git_color_opt}', )) return retval @@ -282,8 +283,8 @@ def run( return 1 if _has_unstaged_config(config_file) and not no_stash: logger.error( - 'Your pre-commit configuration is unstaged.\n' - '`git add {}` to fix this.'.format(config_file), + f'Your pre-commit configuration is unstaged.\n' + f'`git add {config_file}` to fix this.', ) return 1 @@ -308,9 +309,7 @@ def run( if args.hook and not hooks: output.write_line( - 'No hook with id `{}` in stage `{}`'.format( - args.hook, args.hook_stage, - ), + f'No hook with id `{args.hook}` in stage `{args.hook_stage}`', ) return 1 diff --git a/pre_commit/git.py b/pre_commit/git.py index 107a3a3a7..fd8563f14 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -183,13 +183,11 @@ def check_for_cygwin_mismatch() -> None: if is_cygwin_python ^ is_cygwin_git: exe_type = {True: '(cygwin)', False: '(windows)'} logger.warn( - 'pre-commit has detected a mix of cygwin python / git\n' - 'This combination is not supported, it is likely you will ' - 'receive an error later in the program.\n' - 'Make sure to use cygwin git+python while using cygwin\n' - 'These can be installed through the cygwin installer.\n' - ' - python {}\n' - ' - git {}\n'.format( - exe_type[is_cygwin_python], exe_type[is_cygwin_git], - ), + f'pre-commit has detected a mix of cygwin python / git\n' + f'This combination is not supported, it is likely you will ' + f'receive an error later in the program.\n' + f'Make sure to use cygwin git+python while using cygwin\n' + f'These can be installed through the cygwin installer.\n' + f' - python {exe_type[is_cygwin_python]}\n' + f' - git {exe_type[is_cygwin_git]}\n', ) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 4bef33910..00090f118 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -81,7 +81,7 @@ def install_environment( def get_docker_user() -> str: # pragma: windows no cover try: - return '{}:{}'.format(os.getuid(), os.getgid()) + return f'{os.getuid()}:{os.getgid()}' except AttributeError: return '1000:1000' @@ -94,7 +94,7 @@ def docker_cmd() -> Tuple[str, ...]: # pragma: windows no cover # https://docs.docker.com/engine/reference/commandline/run/#mount-volumes-from-container-volumes-from # The `Z` option tells Docker to label the content with a private # unshared label. Only the current container can use a private volume. - '-v', '{}:/src:rw,Z'.format(os.getcwd()), + '-v', f'{os.getcwd()}:/src:rw,Z', '--workdir', '/src', ) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index b39f57aa6..3a9d4d6d5 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -51,8 +51,8 @@ def assert_no_additional_deps( ) -> None: if additional_deps: raise AssertionError( - 'For now, pre-commit does not support ' - 'additional_dependencies for {}'.format(lang), + f'For now, pre-commit does not support ' + f'additional_dependencies for {lang}', ) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index cb73c12ac..34d6c533f 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -33,7 +33,7 @@ def _envdir(prefix: Prefix, version: str) -> str: def get_env_patch(venv: str) -> PatchesT: # pragma: windows no cover if sys.platform == 'cygwin': # pragma: no cover _, win_venv, _ = cmd_output('cygpath', '-w', venv) - install_prefix = r'{}\bin'.format(win_venv.strip()) + install_prefix = fr'{win_venv.strip()}\bin' lib_dir = 'lib' elif sys.platform == 'win32': # pragma: no cover install_prefix = bin_dir(venv) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 6b8463d30..9bdb8e11e 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -39,7 +39,7 @@ def _process_filename_at_once(pattern: Pattern[bytes], filename: str) -> int: if match: retv = 1 line_no = contents[:match.start()].count(b'\n') - output.write('{}:{}:'.format(filename, line_no + 1)) + output.write(f'{filename}:{line_no + 1}:') matched_lines = match.group().split(b'\n') matched_lines[0] = contents.split(b'\n')[line_no] diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 3fad9b9b2..b9078113f 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -47,10 +47,10 @@ def _find_by_py_launcher( version: str, ) -> Optional[str]: # pragma: no cover (windows only) if version.startswith('python'): + num = version[len('python'):] try: return cmd_output( - 'py', '-{}'.format(version[len('python'):]), - '-c', 'import sys; print(sys.executable)', + 'py', f'-{num}', '-c', 'import sys; print(sys.executable)', )[1].strip() except CalledProcessError: pass @@ -88,7 +88,7 @@ def get_default_version() -> str: # pragma: no cover (platform dependent) return exe # Next try the `pythonX.X` executable - exe = 'python{}.{}'.format(*sys.version_info) + exe = f'python{sys.version_info[0]}.{sys.version_info[1]}' if find_executable(exe): return exe @@ -96,7 +96,8 @@ def get_default_version() -> str: # pragma: no cover (platform dependent) return exe # Give a best-effort try for windows - if os.path.exists(r'C:\{}\python.exe'.format(exe.replace('.', ''))): + default_folder_name = exe.replace('.', '') + if os.path.exists(fr'C:\{default_folder_name}\python.exe'): return exe # We tried! @@ -135,7 +136,8 @@ def norm_version(version: str) -> str: # If it is in the form pythonx.x search in the default # place on windows if version.startswith('python'): - return r'C:\{}\python.exe'.format(version.replace('.', '')) + default_folder_name = version.replace('.', '') + return fr'C:\{default_folder_name}\python.exe' # Otherwise assume it is a path return os.path.expanduser(version) diff --git a/pre_commit/logging_handler.py b/pre_commit/logging_handler.py index 807b1177d..ba05295da 100644 --- a/pre_commit/logging_handler.py +++ b/pre_commit/logging_handler.py @@ -21,16 +21,12 @@ def __init__(self, use_color: bool) -> None: self.use_color = use_color def emit(self, record: logging.LogRecord) -> None: - output.write_line( - '{} {}'.format( - color.format_color( - f'[{record.levelname}]', - LOG_LEVEL_COLORS[record.levelname], - self.use_color, - ), - record.getMessage(), - ), + level_msg = color.format_color( + f'[{record.levelname}]', + LOG_LEVEL_COLORS[record.levelname], + self.use_color, ) + output.write_line(f'{level_msg} {record.getMessage()}') @contextlib.contextmanager diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 1359e020f..30b8d8101 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -34,8 +34,7 @@ def check_useless_excludes(config_file: str) -> int: exclude = config['exclude'] if not exclude_matches_any(classifier.filenames, '', exclude): print( - 'The global exclude pattern {!r} does not match any files' - .format(exclude), + f'The global exclude pattern {exclude!r} does not match any files', ) retv = 1 @@ -50,8 +49,8 @@ def check_useless_excludes(config_file: str) -> int: include, exclude = hook['files'], hook['exclude'] if not exclude_matches_any(names, include, exclude): print( - 'The exclude pattern {!r} for {} does not match any files' - .format(exclude, hook['id']), + f'The exclude pattern {exclude!r} for {hook["id"]} does ' + f'not match any files', ) retv = 1 diff --git a/pre_commit/output.py b/pre_commit/output.py index 88857ff16..5d262839b 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -54,12 +54,9 @@ def get_hook_message( assert end_msg is not None assert end_color is not None assert use_color is not None - return '{}{}{}{}\n'.format( - start, - '.' * (cols - len(start) - len(postfix) - len(end_msg) - 1), - postfix, - color.format_color(end_msg, end_color, use_color), - ) + dots = '.' * (cols - len(start) - len(postfix) - len(end_msg) - 1) + end = color.format_color(end_msg, end_color, use_color) + return f'{start}{dots}{postfix}{end}\n' def write(s: str, stream: IO[bytes] = sys.stdout.buffer) -> None: diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 83ed70273..08d8647ca 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -137,8 +137,8 @@ def create(cls, src: str, prefix: Prefix, dct: Dict[str, Any]) -> 'Hook': extra_keys = set(dct) - set(_KEYS) if extra_keys: logger.warning( - 'Unexpected key(s) present on {} => {}: ' - '{}'.format(src, dct['id'], ', '.join(sorted(extra_keys))), + f'Unexpected key(s) present on {src} => {dct["id"]}: ' + f'{", ".join(sorted(extra_keys))}', ) return cls(src=src, prefix=prefix, **{k: dct[k] for k in _KEYS}) @@ -154,11 +154,9 @@ def _hook( version = ret['minimum_pre_commit_version'] if parse_version(version) > parse_version(C.VERSION): logger.error( - 'The hook `{}` requires pre-commit version {} but version {} ' - 'is installed. ' - 'Perhaps run `pip install --upgrade pre-commit`.'.format( - ret['id'], version, C.VERSION, - ), + f'The hook `{ret["id"]}` requires pre-commit version {version} ' + f'but version {C.VERSION} is installed. ' + f'Perhaps run `pip install --upgrade pre-commit`.', ) exit(1) @@ -210,10 +208,9 @@ def _cloned_repository_hooks( for hook in repo_config['hooks']: if hook['id'] not in by_id: logger.error( - '`{}` is not present in repository {}. ' - 'Typo? Perhaps it is introduced in a newer version? ' - 'Often `pre-commit autoupdate` fixes this.' - .format(hook['id'], repo), + f'`{hook["id"]}` is not present in repository {repo}. ' + f'Typo? Perhaps it is introduced in a newer version? ' + f'Often `pre-commit autoupdate` fixes this.', ) exit(1) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 68e796902..213d16eef 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -52,12 +52,11 @@ def _norm_exe(exe: str) -> Tuple[str, ...]: def _run_legacy() -> Tuple[int, bytes]: if __file__.endswith('.legacy'): raise SystemExit( - "bug: pre-commit's script is installed in migration mode\n" - 'run `pre-commit install -f --hook-type {}` to fix this\n\n' - 'Please report this bug at ' - 'https://github.com/pre-commit/pre-commit/issues'.format( - HOOK_TYPE, - ), + f"bug: pre-commit's script is installed in migration mode\n" + f'run `pre-commit install -f --hook-type {HOOK_TYPE}` to fix ' + f'this\n\n' + f'Please report this bug at ' + f'https://github.com/pre-commit/pre-commit/issues', ) if HOOK_TYPE == 'pre-push': @@ -82,20 +81,17 @@ def _validate_config() -> None: if os.path.isfile(cfg): pass elif SKIP_ON_MISSING_CONFIG or os.getenv('PRE_COMMIT_ALLOW_NO_CONFIG'): - print( - '`{}` config file not found. ' - 'Skipping `pre-commit`.'.format(CONFIG), - ) + print(f'`{CONFIG}` config file not found. Skipping `pre-commit`.') raise EarlyExit() else: raise FatalError( - 'No {} file was found\n' - '- To temporarily silence this, run ' - '`PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`\n' - '- To permanently silence this, install pre-commit with the ' - '--allow-missing-config option\n' - '- To uninstall pre-commit run ' - '`pre-commit uninstall`'.format(CONFIG), + f'No {CONFIG} file was found\n' + f'- To temporarily silence this, run ' + f'`PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`\n' + f'- To permanently silence this, install pre-commit with the ' + f'--allow-missing-config option\n' + f'- To uninstall pre-commit run ' + f'`pre-commit uninstall`', ) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 7f3fff0af..832f6768e 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -48,7 +48,7 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: retcode=None, ) if retcode and diff_stdout_binary.strip(): - patch_filename = 'patch{}'.format(int(time.time())) + patch_filename = f'patch{int(time.time())}' patch_filename = os.path.join(patch_dir, patch_filename) logger.warning('Unstaged files detected.') logger.info( diff --git a/pre_commit/store.py b/pre_commit/store.py index 407723c8d..665a6d4bb 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -102,7 +102,7 @@ def connect( @classmethod def db_repo_name(cls, repo: str, deps: Sequence[str]) -> str: if deps: - return '{}:{}'.format(repo, ','.join(sorted(deps))) + return f'{repo}:{",".join(sorted(deps))}' else: return repo diff --git a/pre_commit/util.py b/pre_commit/util.py index 2b3b5b3ee..54ae7ece1 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -105,11 +105,9 @@ def _indent_or_none(part: Optional[bytes]) -> bytes: return b' (none)' return b''.join(( - 'command: {!r}\n' - 'return code: {}\n' - 'expected return code: {}\n'.format( - self.cmd, self.returncode, self.expected_returncode, - ).encode(), + f'command: {self.cmd!r}\n'.encode(), + f'return code: {self.returncode}\n'.encode(), + f'expected return code: {self.expected_returncode}\n'.encode(), b'stdout:', _indent_or_none(self.stdout), b'\n', b'stderr:', _indent_or_none(self.stderr), )) diff --git a/testing/resources/stdout_stderr_repo/stdout-stderr-entry b/testing/resources/stdout_stderr_repo/stdout-stderr-entry index d383c191f..7563df53c 100755 --- a/testing/resources/stdout_stderr_repo/stdout-stderr-entry +++ b/testing/resources/stdout_stderr_repo/stdout-stderr-entry @@ -1,13 +1,7 @@ -#!/usr/bin/env python -import sys - - -def main(): - for i in range(6): - f = sys.stdout if i % 2 == 0 else sys.stderr - f.write(f'{i}\n') - f.flush() - - -if __name__ == '__main__': - exit(main()) +#!/usr/bin/env bash +echo 0 +echo 1 1>&2 +echo 2 +echo 3 1>&2 +echo 4 +echo 5 1>&2 diff --git a/testing/resources/stdout_stderr_repo/tty-check-entry b/testing/resources/stdout_stderr_repo/tty-check-entry index 8c6530ec8..01a9d3883 100755 --- a/testing/resources/stdout_stderr_repo/tty-check-entry +++ b/testing/resources/stdout_stderr_repo/tty-check-entry @@ -1,12 +1,11 @@ -#!/usr/bin/env python -import sys - - -def main(): - print('stdin: {}'.format(sys.stdin.isatty())) - print('stdout: {}'.format(sys.stdout.isatty())) - print('stderr: {}'.format(sys.stderr.isatty())) - - -if __name__ == '__main__': - exit(main()) +#!/usr/bin/env bash +t() { + if [ -t "$1" ]; then + echo "$2: True" + else + echo "$2: False" + fi +} +t 0 stdin +t 1 stdout +t 2 stderr diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 8499c3dda..c48adbde9 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -291,13 +291,11 @@ def test_minimum_pre_commit_version_failing(): cfg = {'repos': [], 'minimum_pre_commit_version': '999'} cfgv.validate(cfg, CONFIG_SCHEMA) assert str(excinfo.value) == ( - '\n' - '==> At Config()\n' - '==> At key: minimum_pre_commit_version\n' - '=====> pre-commit version 999 is required but version {} is ' - 'installed. Perhaps run `pip install --upgrade pre-commit`.'.format( - C.VERSION, - ) + f'\n' + f'==> At Config()\n' + f'==> At key: minimum_pre_commit_version\n' + f'=====> pre-commit version 999 is required but version {C.VERSION} ' + f'is installed. Perhaps run `pip install --upgrade pre-commit`.' ) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index b126cff7c..2c7b2f1fa 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -1,4 +1,4 @@ -import pipes +import shlex import pytest @@ -118,12 +118,12 @@ def test_rev_info_update_does_not_freeze_if_already_sha(out_of_date): def test_autoupdate_up_to_date_repo(up_to_date, tmpdir, store): contents = ( - 'repos:\n' - '- repo: {}\n' - ' rev: {}\n' - ' hooks:\n' - ' - id: foo\n' - ).format(up_to_date, git.head_rev(up_to_date)) + f'repos:\n' + f'- repo: {up_to_date}\n' + f' rev: {git.head_rev(up_to_date)}\n' + f' hooks:\n' + f' - id: foo\n' + ) cfg = tmpdir.join(C.CONFIG_FILE) cfg.write(contents) @@ -278,7 +278,7 @@ def test_loses_formatting_when_not_detectable(out_of_date, store, tmpdir): ' ],\n' ' }}\n' ']\n'.format( - pipes.quote(out_of_date.path), out_of_date.original_rev, + shlex.quote(out_of_date.path), out_of_date.original_rev, ) ) cfg = tmpdir.join(C.CONFIG_FILE) @@ -286,12 +286,12 @@ def test_loses_formatting_when_not_detectable(out_of_date, store, tmpdir): assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 expected = ( - 'repos:\n' - '- repo: {}\n' - ' rev: {}\n' - ' hooks:\n' - ' - id: foo\n' - ).format(out_of_date.path, out_of_date.head_rev) + f'repos:\n' + f'- repo: {out_of_date.path}\n' + f' rev: {out_of_date.head_rev}\n' + f' hooks:\n' + f' - id: foo\n' + ) assert cfg.read() == expected @@ -358,12 +358,12 @@ def test_hook_disppearing_repo_raises(hook_disappearing, store): def test_autoupdate_hook_disappearing_repo(hook_disappearing, tmpdir, store): contents = ( - 'repos:\n' - '- repo: {}\n' - ' rev: {}\n' - ' hooks:\n' - ' - id: foo\n' - ).format(hook_disappearing.path, hook_disappearing.original_rev) + f'repos:\n' + f'- repo: {hook_disappearing.path}\n' + f' rev: {hook_disappearing.original_rev}\n' + f' hooks:\n' + f' - id: foo\n' + ) cfg = tmpdir.join(C.CONFIG_FILE) cfg.write(contents) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index feef316e4..ff2b31838 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -52,11 +52,11 @@ def test_shebang_posix_not_on_path(): def test_shebang_posix_on_path(tmpdir): - tmpdir.join('python{}'.format(sys.version_info[0])).ensure() + tmpdir.join(f'python{sys.version_info[0]}').ensure() with mock.patch.object(sys, 'platform', 'posix'): with mock.patch.object(os, 'defpath', tmpdir.strpath): - expected = '#!/usr/bin/env python{}'.format(sys.version_info[0]) + expected = f'#!/usr/bin/env python{sys.version_info[0]}' assert shebang() == expected diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index d271575e7..b08054f55 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -1,5 +1,5 @@ import os.path -import pipes +import shlex import sys import time from unittest import mock @@ -580,8 +580,7 @@ def test_lots_of_files(store, tempdir_factory): # Write a crap ton of files for i in range(400): - filename = '{}{}'.format('a' * 100, i) - open(filename, 'w').close() + open(f'{"a" * 100}{i}', 'w').close() cmd_output('git', 'add', '.') install(C.CONFIG_FILE, store, hook_types=['pre-commit']) @@ -673,7 +672,7 @@ def test_local_hook_passes(cap_out, store, repo_with_passing_hook): 'id': 'identity-copy', 'name': 'identity-copy', 'entry': '{} -m pre_commit.meta_hooks.identity'.format( - pipes.quote(sys.executable), + shlex.quote(sys.executable), ), 'language': 'system', 'files': r'\.py$', @@ -893,7 +892,7 @@ def test_args_hook_only(cap_out, store, repo_with_passing_hook): 'id': 'identity-copy', 'name': 'identity-copy', 'entry': '{} -m pre_commit.meta_hooks.identity'.format( - pipes.quote(sys.executable), + shlex.quote(sys.executable), ), 'language': 'system', 'files': r'\.py$', diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index fa2fc2d35..8fa41a704 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -99,9 +99,7 @@ def test_log_and_exit(cap_out, mock_store_dir): printed = cap_out.get() log_file = os.path.join(mock_store_dir, 'pre-commit.log') - assert printed == ( - 'msg: FatalError: hai\n' 'Check the log at {}\n'.format(log_file) - ) + assert printed == f'msg: FatalError: hai\nCheck the log at {log_file}\n' assert os.path.exists(log_file) with open(log_file) as f: diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 497b01d65..2739873c4 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -1,5 +1,5 @@ import os.path -import pipes +import shlex from pre_commit.languages.ruby import _install_rbenv from pre_commit.prefix import Prefix @@ -21,7 +21,7 @@ def test_install_rbenv(tempdir_factory): cmd_output( 'bash', '-c', '. {} && rbenv --help'.format( - pipes.quote(prefix.path('rbenv-default', 'bin', 'activate')), + shlex.quote(prefix.path('rbenv-default', 'bin', 'activate')), ), ) @@ -35,6 +35,6 @@ def test_install_rbenv_with_version(tempdir_factory): cmd_output( 'bash', '-c', '. {} && rbenv install --help'.format( - pipes.quote(prefix.path('rbenv-1.9.3p547', 'bin', 'activate')), + shlex.quote(prefix.path('rbenv-1.9.3p547', 'bin', 'activate')), ), ) diff --git a/tests/repository_test.py b/tests/repository_test.py index 5c541c66a..f3ca6c5b4 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -805,9 +805,9 @@ def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): with pytest.raises(SystemExit): _get_hook(config, store, 'i-dont-exist') assert fake_log_handler.handle.call_args[0][0].msg == ( - '`i-dont-exist` is not present in repository file://{}. ' - 'Typo? Perhaps it is introduced in a newer version? ' - 'Often `pre-commit autoupdate` fixes this.'.format(path) + f'`i-dont-exist` is not present in repository file://{path}. ' + f'Typo? Perhaps it is introduced in a newer version? ' + f'Often `pre-commit autoupdate` fixes this.' ) From 5d767bbc499238bd866e091260d543006c718fab Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 11:15:23 -0800 Subject: [PATCH 525/544] Replace match.group(n) with match[n] --- pre_commit/commands/autoupdate.py | 4 ++-- pre_commit/languages/pygrep.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 19e82a06a..fd98118ab 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -121,10 +121,10 @@ def _write_new_config(path: str, rev_infos: List[Optional[RevInfo]]) -> None: new_rev = new_rev_s.split(':', 1)[1].strip() if rev_info.frozen is not None: comment = f' # frozen: {rev_info.frozen}' - elif match.group(4).strip().startswith('# frozen:'): + elif match[4].strip().startswith('# frozen:'): comment = '' else: - comment = match.group(4) + comment = match[4] lines[idx] = f'{match[1]}rev:{match[2]}{new_rev}{comment}{match[5]}' with open(path, 'w') as f: diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 9bdb8e11e..06d91903b 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -41,7 +41,7 @@ def _process_filename_at_once(pattern: Pattern[bytes], filename: str) -> int: line_no = contents[:match.start()].count(b'\n') output.write(f'{filename}:{line_no + 1}:') - matched_lines = match.group().split(b'\n') + matched_lines = match[0].split(b'\n') matched_lines[0] = contents.split(b'\n')[line_no] output.write_line(b'\n'.join(matched_lines)) From 5e52a657df968bfc5733b011faf113693a7f83eb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 11:19:02 -0800 Subject: [PATCH 526/544] Remove unused ruby activate script --- pre_commit/languages/ruby.py | 23 ----------------------- tests/languages/ruby_test.py | 26 +++++++------------------- 2 files changed, 7 insertions(+), 42 deletions(-) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 9f98bea7b..fb3ba9314 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -79,29 +79,6 @@ def _install_rbenv( _extract_resource('ruby-download.tar.gz', plugins_dir) _extract_resource('ruby-build.tar.gz', plugins_dir) - activate_path = prefix.path(directory, 'bin', 'activate') - with open(activate_path, 'w') as activate_file: - # This is similar to how you would install rbenv to your home directory - # However we do a couple things to make the executables exposed and - # configure it to work in our directory. - # We also modify the PS1 variable for manual debugging sake. - activate_file.write( - '#!/usr/bin/env bash\n' - "export RBENV_ROOT='{directory}'\n" - 'export PATH="$RBENV_ROOT/bin:$PATH"\n' - 'eval "$(rbenv init -)"\n' - 'export PS1="(rbenv)$PS1"\n' - # This lets us install gems in an isolated and repeatable - # directory - "export GEM_HOME='{directory}/gems'\n" - 'export PATH="$GEM_HOME/bin:$PATH"\n' - '\n'.format(directory=prefix.path(directory)), - ) - - # If we aren't using the system ruby, add a version here - if version != C.DEFAULT: - activate_file.write(f'export RBENV_VERSION="{version}"\n') - def _install_ruby( prefix: Prefix, diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 2739873c4..36a029d17 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -1,7 +1,6 @@ import os.path -import shlex -from pre_commit.languages.ruby import _install_rbenv +from pre_commit.languages import ruby from pre_commit.prefix import Prefix from pre_commit.util import cmd_output from testing.util import xfailif_windows_no_ruby @@ -10,31 +9,20 @@ @xfailif_windows_no_ruby def test_install_rbenv(tempdir_factory): prefix = Prefix(tempdir_factory.get()) - _install_rbenv(prefix) + ruby._install_rbenv(prefix) # Should have created rbenv directory assert os.path.exists(prefix.path('rbenv-default')) - # We should have created our `activate` script - activate_path = prefix.path('rbenv-default', 'bin', 'activate') - assert os.path.exists(activate_path) # Should be able to activate using our script and access rbenv - cmd_output( - 'bash', '-c', - '. {} && rbenv --help'.format( - shlex.quote(prefix.path('rbenv-default', 'bin', 'activate')), - ), - ) + with ruby.in_env(prefix, 'default'): + cmd_output('rbenv', '--help') @xfailif_windows_no_ruby def test_install_rbenv_with_version(tempdir_factory): prefix = Prefix(tempdir_factory.get()) - _install_rbenv(prefix, version='1.9.3p547') + ruby._install_rbenv(prefix, version='1.9.3p547') # Should be able to activate and use rbenv install - cmd_output( - 'bash', '-c', - '. {} && rbenv install --help'.format( - shlex.quote(prefix.path('rbenv-1.9.3p547', 'bin', 'activate')), - ), - ) + with ruby.in_env(prefix, '1.9.3p547'): + cmd_output('rbenv', 'install', '--help') From f33716cc17fe956727e34edd846bbda4e60fb2b3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 11:21:04 -0800 Subject: [PATCH 527/544] Remove usage of OrderedDict --- pre_commit/commands/try_repo.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index 767d2d065..5e7c667d2 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -1,5 +1,4 @@ import argparse -import collections import logging import os.path from typing import Tuple @@ -62,8 +61,7 @@ def try_repo(args: argparse.Namespace) -> int: manifest = sorted(manifest, key=lambda hook: hook['id']) hooks = [{'id': hook['id']} for hook in manifest] - items = (('repo', repo), ('rev', ref), ('hooks', hooks)) - config = {'repos': [collections.OrderedDict(items)]} + config = {'repos': [{'repo': repo, 'rev': ref, 'hooks': hooks}]} config_s = ordered_dump(config, **C.YAML_DUMP_KWARGS) config_filename = os.path.join(tempdir, C.CONFIG_FILE) From 67c2dcd90d5d2496d9974cc42de430cdd416ea11 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 11:44:41 -0800 Subject: [PATCH 528/544] Remove pre_commit.five --- pre_commit/commands/run.py | 2 +- pre_commit/error_handler.py | 22 ++++++++++------------ pre_commit/five.py | 12 ------------ pre_commit/languages/pygrep.py | 4 ++-- pre_commit/main.py | 2 -- pre_commit/output.py | 15 +++++++++------ pre_commit/repository.py | 3 +-- pre_commit/util.py | 17 +++-------------- tests/conftest.py | 7 +++---- tests/repository_test.py | 9 ++++----- 10 files changed, 33 insertions(+), 60 deletions(-) delete mode 100644 pre_commit/five.py diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 1b08df913..95dd28b65 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -173,7 +173,7 @@ def _run_single_hook( if out.strip(): output.write_line() - output.write_line(out.strip(), logfile_name=hook.log_file) + output.write_line_b(out.strip(), logfile_name=hook.log_file) output.write_line() return files_modified or bool(retcode) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 44e19fd41..77b35698e 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -3,10 +3,9 @@ import sys import traceback from typing import Generator -from typing import Union +from typing import Optional import pre_commit.constants as C -from pre_commit import five from pre_commit import output from pre_commit.store import Store @@ -15,25 +14,24 @@ class FatalError(RuntimeError): pass -def _to_bytes(exc: BaseException) -> bytes: - return str(exc).encode() - - def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None: error_msg = b''.join(( - five.to_bytes(msg), b': ', - five.to_bytes(type(exc).__name__), b': ', - _to_bytes(exc), + msg.encode(), b': ', + type(exc).__name__.encode(), b': ', + str(exc).encode(), )) - output.write_line(error_msg) + output.write_line_b(error_msg) store = Store() log_path = os.path.join(store.directory, 'pre-commit.log') output.write_line(f'Check the log at {log_path}') with open(log_path, 'wb') as log: - def _log_line(s: Union[None, str, bytes] = None) -> None: + def _log_line(s: Optional[str] = None) -> None: output.write_line(s, stream=log) + def _log_line_b(s: Optional[bytes] = None) -> None: + output.write_line_b(s, stream=log) + _log_line('### version information') _log_line() _log_line('```') @@ -50,7 +48,7 @@ def _log_line(s: Union[None, str, bytes] = None) -> None: _log_line('### error information') _log_line() _log_line('```') - _log_line(error_msg) + _log_line_b(error_msg) _log_line('```') _log_line() _log_line('```') diff --git a/pre_commit/five.py b/pre_commit/five.py deleted file mode 100644 index a7ffd9780..000000000 --- a/pre_commit/five.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import Union - - -def to_text(s: Union[str, bytes]) -> str: - return s if isinstance(s, str) else s.decode() - - -def to_bytes(s: Union[str, bytes]) -> bytes: - return s if isinstance(s, bytes) else s.encode() - - -n = to_text diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 06d91903b..c6d1131df 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -27,7 +27,7 @@ def _process_filename_by_line(pattern: Pattern[bytes], filename: str) -> int: if pattern.search(line): retv = 1 output.write(f'{filename}:{line_no}:') - output.write_line(line.rstrip(b'\r\n')) + output.write_line_b(line.rstrip(b'\r\n')) return retv @@ -44,7 +44,7 @@ def _process_filename_at_once(pattern: Pattern[bytes], filename: str) -> int: matched_lines = match[0].split(b'\n') matched_lines[0] = contents.split(b'\n')[line_no] - output.write_line(b'\n'.join(matched_lines)) + output.write_line_b(b'\n'.join(matched_lines)) return retv diff --git a/pre_commit/main.py b/pre_commit/main.py index ce902c07e..eae4f9096 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -9,7 +9,6 @@ import pre_commit.constants as C from pre_commit import color -from pre_commit import five from pre_commit import git from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.clean import clean @@ -155,7 +154,6 @@ def _adjust_args_and_chdir(args: argparse.Namespace) -> None: def main(argv: Optional[Sequence[str]] = None) -> int: argv = argv if argv is not None else sys.argv[1:] - argv = [five.to_text(arg) for arg in argv] parser = argparse.ArgumentParser(prog='pre-commit') # https://stackoverflow.com/a/8521644/812183 diff --git a/pre_commit/output.py b/pre_commit/output.py index 5d262839b..b20b8ab4e 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -1,11 +1,10 @@ import contextlib import sys +from typing import Any from typing import IO from typing import Optional -from typing import Union from pre_commit import color -from pre_commit import five def get_hook_message( @@ -60,12 +59,12 @@ def get_hook_message( def write(s: str, stream: IO[bytes] = sys.stdout.buffer) -> None: - stream.write(five.to_bytes(s)) + stream.write(s.encode()) stream.flush() -def write_line( - s: Union[None, str, bytes] = None, +def write_line_b( + s: Optional[bytes] = None, stream: IO[bytes] = sys.stdout.buffer, logfile_name: Optional[str] = None, ) -> None: @@ -77,6 +76,10 @@ def write_line( for output_stream in output_streams: if s is not None: - output_stream.write(five.to_bytes(s)) + output_stream.write(s) output_stream.write(b'\n') output_stream.flush() + + +def write_line(s: Optional[str] = None, **kwargs: Any) -> None: + write_line_b(s.encode() if s is not None else s, **kwargs) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 08d8647ca..9b0710899 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -12,7 +12,6 @@ from typing import Tuple import pre_commit.constants as C -from pre_commit import five from pre_commit.clientlib import load_manifest from pre_commit.clientlib import LOCAL from pre_commit.clientlib import MANIFEST_HOOK_DICT @@ -49,7 +48,7 @@ def _write_state(prefix: Prefix, venv: str, state: object) -> None: state_filename = _state_filename(prefix, venv) staging = state_filename + 'staging' with open(staging, 'w') as state_file: - state_file.write(five.to_text(json.dumps(state))) + state_file.write(json.dumps(state)) # Move the file into place atomically to indicate we've installed os.rename(staging, state_filename) diff --git a/pre_commit/util.py b/pre_commit/util.py index 54ae7ece1..f5858be2f 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -17,7 +17,6 @@ from typing import Type from typing import Union -from pre_commit import five from pre_commit import parse_shebang if sys.version_info >= (3, 7): # pragma: no cover (PY37+) @@ -116,19 +115,9 @@ def __str__(self) -> str: return self.__bytes__().decode() -def _cmd_kwargs( - *cmd: str, - **kwargs: Any, -) -> Tuple[Tuple[str, ...], Dict[str, Any]]: - # py2/py3 on windows are more strict about the types here - cmd = tuple(five.n(arg) for arg in cmd) - kwargs['env'] = { - five.n(key): five.n(value) - for key, value in kwargs.pop('env', {}).items() - } or None +def _setdefault_kwargs(kwargs: Dict[str, Any]) -> None: for arg in ('stdin', 'stdout', 'stderr'): kwargs.setdefault(arg, subprocess.PIPE) - return cmd, kwargs def cmd_output_b( @@ -136,7 +125,7 @@ def cmd_output_b( **kwargs: Any, ) -> Tuple[int, bytes, Optional[bytes]]: retcode = kwargs.pop('retcode', 0) - cmd, kwargs = _cmd_kwargs(*cmd, **kwargs) + _setdefault_kwargs(kwargs) try: cmd = parse_shebang.normalize_cmd(cmd) @@ -205,7 +194,7 @@ def cmd_output_p( ) -> Tuple[int, bytes, Optional[bytes]]: assert kwargs.pop('retcode') is None assert kwargs['stderr'] == subprocess.STDOUT, kwargs['stderr'] - cmd, kwargs = _cmd_kwargs(*cmd, **kwargs) + _setdefault_kwargs(kwargs) try: cmd = parse_shebang.normalize_cmd(cmd) diff --git a/tests/conftest.py b/tests/conftest.py index 8149bb9ae..335d2614f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -256,10 +256,9 @@ def get(self): def cap_out(): stream = FakeStream() write = functools.partial(output.write, stream=stream) - write_line = functools.partial(output.write_line, stream=stream) - with mock.patch.object(output, 'write', write): - with mock.patch.object(output, 'write_line', write_line): - yield Fixture(stream) + write_line_b = functools.partial(output.write_line_b, stream=stream) + with mock.patch.multiple(output, write=write, write_line_b=write_line_b): + yield Fixture(stream) @pytest.fixture diff --git a/tests/repository_test.py b/tests/repository_test.py index f3ca6c5b4..7a22dee64 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -10,7 +10,6 @@ import pytest import pre_commit.constants as C -from pre_commit import five from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest from pre_commit.envcontext import envcontext @@ -119,7 +118,7 @@ def test_python_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'python_hooks_repo', 'foo', [os.devnull], - b"['" + five.to_bytes(os.devnull) + b"']\nHello World\n", + f'[{os.devnull!r}]\nHello World\n'.encode(), ) @@ -154,7 +153,7 @@ def test_python_hook_weird_setup_cfg(in_git_dir, tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'python_hooks_repo', 'foo', [os.devnull], - b"['" + five.to_bytes(os.devnull) + b"']\nHello World\n", + f'[{os.devnull!r}]\nHello World\n'.encode(), ) @@ -163,7 +162,7 @@ def test_python_venv(tempdir_factory, store): # pragma: no cover (no venv) _test_hook_repo( tempdir_factory, store, 'python_venv_hooks_repo', 'foo', [os.devnull], - b"['" + five.to_bytes(os.devnull) + b"']\nHello World\n", + f'[{os.devnull!r}]\nHello World\n'.encode(), ) @@ -188,7 +187,7 @@ def test_versioned_python_hook(tempdir_factory, store): tempdir_factory, store, 'python3_hooks_repo', 'python3-hook', [os.devnull], - b"3\n['" + five.to_bytes(os.devnull) + b"']\nHello World\n", + f'3\n[{os.devnull!r}]\nHello World\n'.encode(), ) From 2a9893d0f07ebf853a45737c4c1914046f985505 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 11:50:40 -0800 Subject: [PATCH 529/544] mkdirp -> os.makedirs(..., exist_ok=True) --- pre_commit/commands/install_uninstall.py | 3 +-- pre_commit/staged_files_only.py | 3 +-- pre_commit/store.py | 3 +-- pre_commit/util.py | 8 -------- tests/commands/install_uninstall_test.py | 13 ++++++------- 5 files changed, 9 insertions(+), 21 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 717acb071..7aeba2286 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -14,7 +14,6 @@ from pre_commit.repository import install_hook_envs from pre_commit.store import Store from pre_commit.util import make_executable -from pre_commit.util import mkdirp from pre_commit.util import resource_text @@ -78,7 +77,7 @@ def _install_hook_script( ) -> None: hook_path, legacy_path = _hook_paths(hook_type, git_dir=git_dir) - mkdirp(os.path.dirname(hook_path)) + os.makedirs(os.path.dirname(hook_path), exist_ok=True) # If we have an existing hook, move it to pre-commit.legacy if os.path.lexists(hook_path) and not is_our_script(hook_path): diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 832f6768e..22608e59a 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -8,7 +8,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 mkdirp from pre_commit.xargs import xargs @@ -55,7 +54,7 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: f'Stashing unstaged files to {patch_filename}.', ) # Save the current unstaged changes as a patch - mkdirp(patch_dir) + os.makedirs(patch_dir, exist_ok=True) with open(patch_filename, 'wb') as patch_file: patch_file.write(diff_stdout_binary) diff --git a/pre_commit/store.py b/pre_commit/store.py index 665a6d4bb..4af161937 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -16,7 +16,6 @@ from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b -from pre_commit.util import mkdirp from pre_commit.util import resource_text from pre_commit.util import rmtree @@ -45,7 +44,7 @@ def __init__(self, directory: Optional[str] = None) -> None: self.db_path = os.path.join(self.directory, 'db.db') if not os.path.exists(self.directory): - mkdirp(self.directory) + os.makedirs(self.directory, exist_ok=True) with open(os.path.join(self.directory, 'README'), 'w') as f: f.write( 'This directory is maintained by the pre-commit project.\n' diff --git a/pre_commit/util.py b/pre_commit/util.py index f5858be2f..468a4b7da 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -29,14 +29,6 @@ EnvironT = Union[Dict[str, str], 'os._Environ'] -def mkdirp(path: str) -> None: - try: - os.makedirs(path) - except OSError: - if not os.path.exists(path): - raise - - @contextlib.contextmanager def clean_path_on_failure(path: str) -> Generator[None, None, None]: """Cleans up the directory on an exceptional failure.""" diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index ff2b31838..cb17f004c 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -14,7 +14,6 @@ from pre_commit.parse_shebang import find_executable from pre_commit.util import cmd_output from pre_commit.util import make_executable -from pre_commit.util import mkdirp from pre_commit.util import resource_text from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo @@ -307,7 +306,7 @@ def test_failing_hooks_returns_nonzero(tempdir_factory, store): def _write_legacy_hook(path): - mkdirp(os.path.join(path, '.git/hooks')) + os.makedirs(os.path.join(path, '.git/hooks'), exist_ok=True) with open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: f.write('#!/usr/bin/env bash\necho "legacy hook"\n') make_executable(f.name) @@ -370,7 +369,7 @@ def test_failing_existing_hook_returns_1(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): # Write out a failing "old" hook - mkdirp(os.path.join(path, '.git/hooks')) + os.makedirs(os.path.join(path, '.git/hooks'), exist_ok=True) with open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: f.write('#!/usr/bin/env bash\necho "fail!"\nexit 1\n') make_executable(f.name) @@ -432,7 +431,7 @@ def test_replace_old_commit_script(tempdir_factory, store): CURRENT_HASH, PRIOR_HASHES[-1], ) - mkdirp(os.path.join(path, '.git/hooks')) + os.makedirs(os.path.join(path, '.git/hooks'), exist_ok=True) with open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: f.write(new_contents) make_executable(f.name) @@ -609,7 +608,7 @@ def test_pre_push_legacy(tempdir_factory, store): path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - mkdirp(os.path.join(path, '.git/hooks')) + os.makedirs(os.path.join(path, '.git/hooks'), exist_ok=True) with open(os.path.join(path, '.git/hooks/pre-push'), 'w') as f: f.write( '#!/usr/bin/env bash\n' @@ -658,7 +657,7 @@ def test_commit_msg_integration_passing( def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): hook_path = os.path.join(commit_msg_repo, '.git/hooks/commit-msg') - mkdirp(os.path.dirname(hook_path)) + os.makedirs(os.path.dirname(hook_path), exist_ok=True) with open(hook_path, 'w') as hook_file: hook_file.write( '#!/usr/bin/env bash\n' @@ -713,7 +712,7 @@ def test_prepare_commit_msg_legacy( hook_path = os.path.join( prepare_commit_msg_repo, '.git/hooks/prepare-commit-msg', ) - mkdirp(os.path.dirname(hook_path)) + os.makedirs(os.path.dirname(hook_path), exist_ok=True) with open(hook_path, 'w') as hook_file: hook_file.write( '#!/usr/bin/env bash\n' From 49cf4906970d11e83448138233f8c7eba33e53fd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 12:08:56 -0800 Subject: [PATCH 530/544] Remove noop_context --- pre_commit/commands/run.py | 17 +++++++++-------- pre_commit/util.py | 5 ----- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 95dd28b65..2cf213a77 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -1,4 +1,5 @@ import argparse +import contextlib import functools import logging import os @@ -27,7 +28,6 @@ from pre_commit.store import Store from pre_commit.util import cmd_output_b from pre_commit.util import EnvironT -from pre_commit.util import noop_context logger = logging.getLogger('pre_commit') @@ -272,7 +272,7 @@ def run( args: argparse.Namespace, environ: EnvironT = os.environ, ) -> int: - no_stash = args.all_files or bool(args.files) + stash = not args.all_files and not args.files # Check if we have unresolved merge conflict files and fail fast. if _has_unmerged_paths(): @@ -281,7 +281,7 @@ def run( if bool(args.source) != bool(args.origin): logger.error('Specify both --origin and --source.') return 1 - if _has_unstaged_config(config_file) and not no_stash: + if stash and _has_unstaged_config(config_file): logger.error( f'Your pre-commit configuration is unstaged.\n' f'`git add {config_file}` to fix this.', @@ -293,12 +293,10 @@ def run( environ['PRE_COMMIT_ORIGIN'] = args.origin environ['PRE_COMMIT_SOURCE'] = args.source - if no_stash: - ctx = noop_context() - else: - ctx = staged_files_only(store.directory) + with contextlib.ExitStack() as exit_stack: + if stash: + exit_stack.enter_context(staged_files_only(store.directory)) - with ctx: config = load_config(config_file) hooks = [ hook @@ -316,3 +314,6 @@ def run( install_hook_envs(hooks, store) return _run_hooks(config, hooks, args, environ) + + # https://github.com/python/mypy/issues/7726 + raise AssertionError('unreachable') diff --git a/pre_commit/util.py b/pre_commit/util.py index 468a4b7da..1fecf2db0 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -40,11 +40,6 @@ def clean_path_on_failure(path: str) -> Generator[None, None, None]: raise -@contextlib.contextmanager -def noop_context() -> Generator[None, None, None]: - yield - - @contextlib.contextmanager def tmpdir() -> Generator[str, None, None]: """Contextmanager to create a temporary directory. It will be cleaned up From 34c3a1580a4fc556eacb5da2e5dd032a9a24ac65 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 12:11:03 -0800 Subject: [PATCH 531/544] unrelated cleanup --- pre_commit/util.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index 1fecf2db0..b829a4837 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -62,9 +62,8 @@ def resource_text(filename: str) -> str: def make_executable(filename: str) -> None: original_mode = os.stat(filename).st_mode - os.chmod( - filename, original_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH, - ) + new_mode = original_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH + os.chmod(filename, new_mode) class CalledProcessError(RuntimeError): From 5779f93ec667aff669045f5660dabe92389f1a0e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 12:19:07 -0800 Subject: [PATCH 532/544] keyword only arguments in some places --- pre_commit/util.py | 5 +++-- pre_commit/xargs.py | 9 +++++---- testing/util.py | 16 ++++++++-------- tests/envcontext_test.py | 7 +------ 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index b829a4837..dfe07ea9c 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -108,9 +108,9 @@ def _setdefault_kwargs(kwargs: Dict[str, Any]) -> None: def cmd_output_b( *cmd: str, + retcode: Optional[int] = 0, **kwargs: Any, ) -> Tuple[int, bytes, Optional[bytes]]: - retcode = kwargs.pop('retcode', 0) _setdefault_kwargs(kwargs) try: @@ -176,9 +176,10 @@ def __exit__( def cmd_output_p( *cmd: str, + retcode: Optional[int] = 0, **kwargs: Any, ) -> Tuple[int, bytes, Optional[bytes]]: - assert kwargs.pop('retcode') is None + assert retcode is None assert kwargs['stderr'] == subprocess.STDOUT, kwargs['stderr'] _setdefault_kwargs(kwargs) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index ccd341d49..5235dc650 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -117,6 +117,10 @@ def _thread_mapper(maxsize: int) -> Generator[ def xargs( cmd: Tuple[str, ...], varargs: Sequence[str], + *, + color: bool = False, + target_concurrency: int = 1, + _max_length: int = _get_platform_max_length(), **kwargs: Any, ) -> Tuple[int, bytes]: """A simplified implementation of xargs. @@ -124,9 +128,6 @@ def xargs( color: Make a pty if on a platform that supports it target_concurrency: Target number of partitions to run concurrently """ - color = kwargs.pop('color', False) - target_concurrency = kwargs.pop('target_concurrency', 1) - max_length = kwargs.pop('_max_length', _get_platform_max_length()) cmd_fn = cmd_output_p if color else cmd_output_b retcode = 0 stdout = b'' @@ -136,7 +137,7 @@ def xargs( except parse_shebang.ExecutableNotFoundError as e: return e.to_output()[:2] - partitions = partition(cmd, varargs, target_concurrency, max_length) + partitions = partition(cmd, varargs, target_concurrency, _max_length) def run_cmd_partition( run_cmd: Tuple[str, ...], diff --git a/testing/util.py b/testing/util.py index dbe475eb9..efeb1e011 100644 --- a/testing/util.py +++ b/testing/util.py @@ -18,13 +18,15 @@ def get_resource_path(path): return os.path.join(TESTING_DIR, 'resources', path) -def cmd_output_mocked_pre_commit_home(*args, **kwargs): - # keyword-only argument - tempdir_factory = kwargs.pop('tempdir_factory') - pre_commit_home = kwargs.pop('pre_commit_home', tempdir_factory.get()) +def cmd_output_mocked_pre_commit_home( + *args, tempdir_factory, pre_commit_home=None, env=None, **kwargs, +): + if pre_commit_home is None: + pre_commit_home = tempdir_factory.get() + env = env if env is not None else os.environ kwargs.setdefault('stderr', subprocess.STDOUT) # Don't want to write to the home directory - env = dict(kwargs.pop('env', os.environ), PRE_COMMIT_HOME=pre_commit_home) + env = dict(env, PRE_COMMIT_HOME=pre_commit_home) ret, out, _ = cmd_output(*args, env=env, **kwargs) return ret, out.replace('\r\n', '\n'), None @@ -123,9 +125,7 @@ def cwd(path): os.chdir(original_cwd) -def git_commit(*args, **kwargs): - fn = kwargs.pop('fn', cmd_output) - msg = kwargs.pop('msg', 'commit!') +def git_commit(*args, fn=cmd_output, msg='commit!', **kwargs): kwargs.setdefault('stderr', subprocess.STDOUT) cmd = ('git', 'commit', '--allow-empty', '--no-gpg-sign', '-a') + args diff --git a/tests/envcontext_test.py b/tests/envcontext_test.py index 56dd26328..f9d4dce69 100644 --- a/tests/envcontext_test.py +++ b/tests/envcontext_test.py @@ -8,12 +8,7 @@ from pre_commit.envcontext import Var -def _test(**kwargs): - before = kwargs.pop('before') - patch = kwargs.pop('patch') - expected = kwargs.pop('expected') - assert not kwargs - +def _test(*, before, patch, expected): env = before.copy() with envcontext(patch, _env=env): assert env == expected From 5706b9149c9e7017bf9134155e1351db8114cdf8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 12:29:50 -0800 Subject: [PATCH 533/544] deep listdir works in python3 on windows --- pre_commit/languages/node.py | 8 ++++---- testing/util.py | 22 ---------------------- tests/repository_test.py | 4 ---- 3 files changed, 4 insertions(+), 30 deletions(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 34d6c533f..914d87972 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -30,7 +30,7 @@ def _envdir(prefix: Prefix, version: str) -> str: return prefix.path(directory) -def get_env_patch(venv: str) -> PatchesT: # pragma: windows no cover +def get_env_patch(venv: str) -> PatchesT: if sys.platform == 'cygwin': # pragma: no cover _, win_venv, _ = cmd_output('cygpath', '-w', venv) install_prefix = fr'{win_venv.strip()}\bin' @@ -54,14 +54,14 @@ def get_env_patch(venv: str) -> PatchesT: # pragma: windows no cover def in_env( prefix: Prefix, language_version: str, -) -> Generator[None, None, None]: # pragma: windows no cover +) -> Generator[None, None, None]: with envcontext(get_env_patch(_envdir(prefix, language_version))): yield def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], -) -> None: # pragma: windows no cover +) -> None: additional_dependencies = tuple(additional_dependencies) assert prefix.exists('package.json') envdir = _envdir(prefix, version) @@ -91,6 +91,6 @@ def run_hook( hook: 'Hook', file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: # pragma: windows no cover +) -> Tuple[int, bytes]: with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/testing/util.py b/testing/util.py index efeb1e011..b318618c0 100644 --- a/testing/util.py +++ b/testing/util.py @@ -1,7 +1,6 @@ import contextlib import os.path import subprocess -import sys import pytest @@ -46,27 +45,6 @@ def cmd_output_mocked_pre_commit_home( xfailif_windows = pytest.mark.xfail(os.name == 'nt', reason='windows') -def broken_deep_listdir(): # pragma: no cover (platform specific) - if sys.platform != 'win32': - return False - try: - os.listdir('\\\\?\\' + os.path.abspath('.')) - except OSError: - return True - try: - os.listdir(b'\\\\?\\C:' + b'\\' * 300) - except TypeError: - return True - except OSError: - return False - - -xfailif_broken_deep_listdir = pytest.mark.xfail( - broken_deep_listdir(), - reason='Node on windows requires deep listdir', -) - - xfailif_no_symlink = pytest.mark.xfail( not hasattr(os, 'symlink'), reason='Symlink is not supported on this platform', diff --git a/tests/repository_test.py b/tests/repository_test.py index 7a22dee64..2dc9e8665 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -32,7 +32,6 @@ from testing.util import get_resource_path from testing.util import skipif_cant_run_docker from testing.util import skipif_cant_run_swift -from testing.util import xfailif_broken_deep_listdir from testing.util import xfailif_no_venv from testing.util import xfailif_windows_no_ruby @@ -230,7 +229,6 @@ def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): ) -@xfailif_broken_deep_listdir def test_run_a_node_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'node_hooks_repo', @@ -238,7 +236,6 @@ def test_run_a_node_hook(tempdir_factory, store): ) -@xfailif_broken_deep_listdir def test_run_versioned_node_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'node_versioned_hooks_repo', @@ -521,7 +518,6 @@ def test_additional_ruby_dependencies_installed(tempdir_factory, store): assert 'tins' in output -@xfailif_broken_deep_listdir # pragma: windows no cover def test_additional_node_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'node_hooks_repo') config = make_config_from_repo(path) From 251721b890a21284deb9a0beab8433c274687730 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 12:30:40 -0800 Subject: [PATCH 534/544] os.symlink is always an attribute in py3 --- testing/util.py | 6 ------ tests/commands/install_uninstall_test.py | 2 -- tests/commands/run_test.py | 2 -- 3 files changed, 10 deletions(-) diff --git a/testing/util.py b/testing/util.py index b318618c0..0c2cc6a89 100644 --- a/testing/util.py +++ b/testing/util.py @@ -45,12 +45,6 @@ def cmd_output_mocked_pre_commit_home( xfailif_windows = pytest.mark.xfail(os.name == 'nt', reason='windows') -xfailif_no_symlink = pytest.mark.xfail( - not hasattr(os, 'symlink'), - reason='Symlink is not supported on this platform', -) - - def supports_venv(): # pragma: no cover (platform specific) try: __import__('ensurepip') diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index cb17f004c..c611bfb62 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -21,7 +21,6 @@ from testing.util import cmd_output_mocked_pre_commit_home from testing.util import cwd from testing.util import git_commit -from testing.util import xfailif_no_symlink from testing.util import xfailif_windows @@ -89,7 +88,6 @@ def test_install_refuses_core_hookspath(in_git_dir, store): assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) -@xfailif_no_symlink # pragma: windows no cover def test_install_hooks_dead_symlink(in_git_dir, store): hook = in_git_dir.join('.git/hooks').ensure_dir().join('pre-commit') os.symlink('/fake/baz', hook.strpath) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index b08054f55..1ed866bcd 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -27,7 +27,6 @@ from testing.util import cwd from testing.util import git_commit from testing.util import run_opts -from testing.util import xfailif_no_symlink @pytest.fixture @@ -861,7 +860,6 @@ def test_include_exclude_base_case(some_filenames): ] -@xfailif_no_symlink # pragma: windows no cover def test_matches_broken_symlink(tmpdir): with tmpdir.as_cwd(): os.symlink('does-not-exist', 'link') From df40e862f4ec4721d2950e29c08e83462cc70ff6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 21:17:59 -0800 Subject: [PATCH 535/544] More miscellaneous cleanup --- .coveragerc | 1 - pre_commit/clientlib.py | 13 ++-- pre_commit/color.py | 64 ++++++++++++++---- pre_commit/color_windows.py | 49 -------------- pre_commit/commands/init_templatedir.py | 4 +- pre_commit/commands/migrate_config.py | 7 +- pre_commit/commands/run.py | 33 +++++++--- pre_commit/commands/try_repo.py | 5 +- pre_commit/constants.py | 7 +- pre_commit/error_handler.py | 21 ++---- pre_commit/git.py | 2 +- pre_commit/languages/conda.py | 6 +- pre_commit/languages/fail.py | 2 +- pre_commit/languages/node.py | 4 +- pre_commit/languages/python.py | 5 +- pre_commit/languages/ruby.py | 8 ++- pre_commit/languages/rust.py | 5 +- pre_commit/languages/script.py | 3 +- pre_commit/main.py | 3 +- pre_commit/make_archives.py | 6 +- pre_commit/output.py | 53 --------------- pre_commit/parse_shebang.py | 3 +- pre_commit/repository.py | 4 +- pre_commit/staged_files_only.py | 4 +- tests/color_test.py | 5 +- tests/commands/install_uninstall_test.py | 28 ++++---- tests/commands/run_test.py | 63 +++++++++++++++++- tests/commands/try_repo_test.py | 2 +- tests/error_handler_test.py | 1 - tests/languages/python_test.py | 2 +- tests/logging_handler_test.py | 2 +- tests/output_test.py | 84 ++---------------------- tests/staged_files_only_test.py | 6 +- 33 files changed, 209 insertions(+), 296 deletions(-) delete mode 100644 pre_commit/color_windows.py diff --git a/.coveragerc b/.coveragerc index 14fb527e7..7cf6cfae3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -7,7 +7,6 @@ omit = setup.py # Don't complain if non-runnable code isn't run */__main__.py - pre_commit/color_windows.py pre_commit/resources/* [report] diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 46ab3cd05..43e2c8ec5 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -192,19 +192,20 @@ def warn_unknown_keys_repo( cfgv.Required('id', cfgv.check_one_of(tuple(k for k, _ in _meta))), # language must be system cfgv.Optional('language', cfgv.check_one_of({'system'}), 'system'), - *([ + *( # default to the hook definition for the meta hooks cfgv.ConditionalOptional(key, cfgv.check_any, value, 'id', hook_id) for hook_id, values in _meta for key, value in values - ] + [ + ), + *( # default to the "manifest" parsing cfgv.OptionalNoDefault(item.key, item.check_fn) # these will always be defaulted above if item.key in {'name', 'language', 'entry'} else item for item in MANIFEST_HOOK_DICT.items - ]), + ), ) CONFIG_HOOK_DICT = cfgv.Map( 'Hook', 'id', @@ -215,11 +216,11 @@ def warn_unknown_keys_repo( # are optional. # No defaults are provided here as the config is merged on top of the # manifest. - *[ + *( cfgv.OptionalNoDefault(item.key, item.check_fn) for item in MANIFEST_HOOK_DICT.items if item.key != 'id' - ], + ), ) CONFIG_REPO_DICT = cfgv.Map( 'Repository', 'repo', @@ -245,7 +246,7 @@ def warn_unknown_keys_repo( DEFAULT_LANGUAGE_VERSION = cfgv.Map( 'DefaultLanguageVersion', None, cfgv.NoAdditionalKeys(all_languages), - *[cfgv.Optional(x, cfgv.check_string, C.DEFAULT) for x in all_languages], + *(cfgv.Optional(x, cfgv.check_string, C.DEFAULT) for x in all_languages), ) CONFIG_SCHEMA = cfgv.Map( 'Config', None, diff --git a/pre_commit/color.py b/pre_commit/color.py index fbb73434f..caf4cb082 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -1,24 +1,64 @@ import os import sys -terminal_supports_color = True if sys.platform == 'win32': # pragma: no cover (windows) - from pre_commit.color_windows import enable_virtual_terminal_processing + def _enable() -> None: + from ctypes import POINTER + from ctypes import windll + from ctypes import WinError + from ctypes import WINFUNCTYPE + from ctypes.wintypes import BOOL + from ctypes.wintypes import DWORD + from ctypes.wintypes import HANDLE + + STD_OUTPUT_HANDLE = -11 + ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 + + def bool_errcheck(result, func, args): + if not result: + raise WinError() + return args + + GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)( + ('GetStdHandle', windll.kernel32), ((1, 'nStdHandle'),), + ) + + GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))( + ('GetConsoleMode', windll.kernel32), + ((1, 'hConsoleHandle'), (2, 'lpMode')), + ) + GetConsoleMode.errcheck = bool_errcheck + + SetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, DWORD)( + ('SetConsoleMode', windll.kernel32), + ((1, 'hConsoleHandle'), (1, 'dwMode')), + ) + SetConsoleMode.errcheck = bool_errcheck + + # As of Windows 10, the Windows console supports (some) ANSI escape + # sequences, but it needs to be enabled using `SetConsoleMode` first. + # + # More info on the escape sequences supported: + # https://msdn.microsoft.com/en-us/library/windows/desktop/mt638032(v=vs.85).aspx + stdout = GetStdHandle(STD_OUTPUT_HANDLE) + flags = GetConsoleMode(stdout) + SetConsoleMode(stdout, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING) + try: - enable_virtual_terminal_processing() + _enable() except OSError: terminal_supports_color = False + else: + terminal_supports_color = True +else: # pragma: windows no cover + terminal_supports_color = True RED = '\033[41m' GREEN = '\033[42m' YELLOW = '\033[43;30m' TURQUOISE = '\033[46;30m' SUBTLE = '\033[2m' -NORMAL = '\033[0m' - - -class InvalidColorSetting(ValueError): - pass +NORMAL = '\033[m' def format_color(text: str, color: str, use_color_setting: bool) -> str: @@ -29,10 +69,10 @@ def format_color(text: str, color: str, use_color_setting: bool) -> str: color - The color start string use_color_setting - Whether or not to color """ - if not use_color_setting: - return text - else: + if use_color_setting: return f'{color}{text}{NORMAL}' + else: + return text COLOR_CHOICES = ('auto', 'always', 'never') @@ -45,7 +85,7 @@ def use_color(setting: str) -> bool: setting - Either `auto`, `always`, or `never` """ if setting not in COLOR_CHOICES: - raise InvalidColorSetting(setting) + raise ValueError(setting) return ( setting == 'always' or ( diff --git a/pre_commit/color_windows.py b/pre_commit/color_windows.py deleted file mode 100644 index 4cbb13413..000000000 --- a/pre_commit/color_windows.py +++ /dev/null @@ -1,49 +0,0 @@ -import sys -assert sys.platform == 'win32' - -from ctypes import POINTER # noqa: E402 -from ctypes import windll # noqa: E402 -from ctypes import WinError # noqa: E402 -from ctypes import WINFUNCTYPE # noqa: E402 -from ctypes.wintypes import BOOL # noqa: E402 -from ctypes.wintypes import DWORD # noqa: E402 -from ctypes.wintypes import HANDLE # noqa: E402 - - -STD_OUTPUT_HANDLE = -11 -ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 - - -def bool_errcheck(result, func, args): - if not result: - raise WinError() - return args - - -GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)( - ('GetStdHandle', windll.kernel32), ((1, 'nStdHandle'),), -) - -GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))( - ('GetConsoleMode', windll.kernel32), - ((1, 'hConsoleHandle'), (2, 'lpMode')), -) -GetConsoleMode.errcheck = bool_errcheck - -SetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, DWORD)( - ('SetConsoleMode', windll.kernel32), - ((1, 'hConsoleHandle'), (1, 'dwMode')), -) -SetConsoleMode.errcheck = bool_errcheck - - -def enable_virtual_terminal_processing(): - """As of Windows 10, the Windows console supports (some) ANSI escape - sequences, but it needs to be enabled using `SetConsoleMode` first. - - More info on the escape sequences supported: - https://msdn.microsoft.com/en-us/library/windows/desktop/mt638032(v=vs.85).aspx - """ - stdout = GetStdHandle(STD_OUTPUT_HANDLE) - flags = GetConsoleMode(stdout) - SetConsoleMode(stdout, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING) diff --git a/pre_commit/commands/init_templatedir.py b/pre_commit/commands/init_templatedir.py index 8ccab55d8..f676fb192 100644 --- a/pre_commit/commands/init_templatedir.py +++ b/pre_commit/commands/init_templatedir.py @@ -29,7 +29,5 @@ def init_templatedir( dest = os.path.realpath(directory) if configured_path != dest: logger.warning('`init.templateDir` not set to the target directory') - logger.warning( - f'maybe `git config --global init.templateDir {dest}`?', - ) + logger.warning(f'maybe `git config --global init.templateDir {dest}`?') return 0 diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index 2e3a29fad..5b90b6f6b 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -28,18 +28,17 @@ def _migrate_map(contents: str) -> str: # If they are using the "default" flow style of yaml, this operation # will yield a valid configuration try: - trial_contents = header + 'repos:\n' + rest + trial_contents = f'{header}repos:\n{rest}' ordered_load(trial_contents) contents = trial_contents except yaml.YAMLError: - contents = header + 'repos:\n' + _indent(rest) + contents = f'{header}repos:\n{_indent(rest)}' return contents def _migrate_sha_to_rev(contents: str) -> str: - reg = re.compile(r'(\n\s+)sha:') - return reg.sub(r'\1rev:', contents) + return re.sub(r'(\n\s+)sha:', r'\1rev:', contents) def migrate_config(config_file: str, quiet: bool = False) -> int: diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 2cf213a77..ce5a06c2e 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -20,7 +20,6 @@ from pre_commit import git from pre_commit import output from pre_commit.clientlib import load_config -from pre_commit.output import get_hook_message from pre_commit.repository import all_hooks from pre_commit.repository import Hook from pre_commit.repository import install_hook_envs @@ -33,6 +32,25 @@ logger = logging.getLogger('pre_commit') +def _start_msg(*, start: str, cols: int, end_len: int) -> str: + dots = '.' * (cols - len(start) - end_len - 1) + return f'{start}{dots}' + + +def _full_msg( + *, + start: str, + cols: int, + end_msg: str, + end_color: str, + use_color: bool, + postfix: str = '', +) -> str: + dots = '.' * (cols - len(start) - len(postfix) - len(end_msg) - 1) + end = color.format_color(end_msg, end_color, use_color) + return f'{start}{dots}{postfix}{end}\n' + + def filter_by_include_exclude( names: Collection[str], include: str, @@ -106,8 +124,8 @@ def _run_single_hook( if hook.id in skips or hook.alias in skips: output.write( - get_hook_message( - hook.name, + _full_msg( + start=hook.name, end_msg=SKIPPED, end_color=color.YELLOW, use_color=use_color, @@ -120,8 +138,8 @@ def _run_single_hook( out = b'' elif not filenames and not hook.always_run: output.write( - get_hook_message( - hook.name, + _full_msg( + start=hook.name, postfix=NO_FILES, end_msg=SKIPPED, end_color=color.TURQUOISE, @@ -135,7 +153,7 @@ def _run_single_hook( out = b'' else: # print hook and dots first in case the hook takes a while to run - output.write(get_hook_message(hook.name, end_len=6, cols=cols)) + output.write(_start_msg(start=hook.name, end_len=6, cols=cols)) diff_cmd = ('git', 'diff', '--no-ext-diff') diff_before = cmd_output_b(*diff_cmd, retcode=None) @@ -218,9 +236,8 @@ def _run_hooks( """Actually run the hooks.""" skips = _get_skips(environ) cols = _compute_cols(hooks) - filenames = _all_filenames(args) filenames = filter_by_include_exclude( - filenames, config['files'], config['exclude'], + _all_filenames(args), config['files'], config['exclude'], ) classifier = Classifier(filenames) retval = 0 diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index 5e7c667d2..989a0c12c 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -1,6 +1,7 @@ import argparse import logging import os.path +from typing import Optional from typing import Tuple from aspy.yaml import ordered_dump @@ -18,9 +19,9 @@ logger = logging.getLogger(__name__) -def _repo_ref(tmpdir: str, repo: str, ref: str) -> Tuple[str, str]: +def _repo_ref(tmpdir: str, repo: str, ref: Optional[str]) -> Tuple[str, str]: # if `ref` is explicitly passed, use it - if ref: + if ref is not None: return repo, ref ref = git.head_rev(repo) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index aad7c498f..0fc740b28 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -8,12 +8,7 @@ CONFIG_FILE = '.pre-commit-config.yaml' MANIFEST_FILE = '.pre-commit-hooks.yaml' -YAML_DUMP_KWARGS = { - 'default_flow_style': False, - # Use unicode - 'encoding': None, - 'indent': 4, -} +YAML_DUMP_KWARGS = {'default_flow_style': False, 'indent': 4} # Bump when installation changes in a backwards / forwards incompatible way INSTALLED_STATE_VERSION = '1' diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 77b35698e..0ea7ed3fb 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -1,9 +1,9 @@ import contextlib +import functools import os.path import sys import traceback from typing import Generator -from typing import Optional import pre_commit.constants as C from pre_commit import output @@ -15,22 +15,13 @@ class FatalError(RuntimeError): def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None: - error_msg = b''.join(( - msg.encode(), b': ', - type(exc).__name__.encode(), b': ', - str(exc).encode(), - )) - output.write_line_b(error_msg) - store = Store() - log_path = os.path.join(store.directory, 'pre-commit.log') + error_msg = f'{msg}: {type(exc).__name__}: {exc}' + output.write_line(error_msg) + log_path = os.path.join(Store().directory, 'pre-commit.log') output.write_line(f'Check the log at {log_path}') with open(log_path, 'wb') as log: - def _log_line(s: Optional[str] = None) -> None: - output.write_line(s, stream=log) - - def _log_line_b(s: Optional[bytes] = None) -> None: - output.write_line_b(s, stream=log) + _log_line = functools.partial(output.write_line, stream=log) _log_line('### version information') _log_line() @@ -48,7 +39,7 @@ def _log_line_b(s: Optional[bytes] = None) -> None: _log_line('### error information') _log_line() _log_line('```') - _log_line_b(error_msg) + _log_line(error_msg) _log_line('```') _log_line() _log_line('```') diff --git a/pre_commit/git.py b/pre_commit/git.py index fd8563f14..72a42545d 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -141,7 +141,7 @@ def head_rev(remote: str) -> str: def has_diff(*args: str, repo: str = '.') -> bool: - cmd = ('git', 'diff', '--quiet', '--no-ext-diff') + args + cmd = ('git', 'diff', '--quiet', '--no-ext-diff', *args) return cmd_output_b(*cmd, cwd=repo, retcode=None)[0] == 1 diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index 6c4c786a9..117a44a46 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -30,9 +30,9 @@ def get_env_patch(env: str) -> PatchesT: # seems to be used for python.exe. path: SubstitutionT = (os.path.join(env, 'bin'), os.pathsep, Var('PATH')) if os.name == 'nt': # pragma: no cover (platform specific) - path = (env, os.pathsep) + path - path = (os.path.join(env, 'Scripts'), os.pathsep) + path - path = (os.path.join(env, 'Library', 'bin'), os.pathsep) + path + path = (env, os.pathsep, *path) + path = (os.path.join(env, 'Scripts'), os.pathsep, *path) + path = (os.path.join(env, 'Library', 'bin'), os.pathsep, *path) return ( ('PYTHONHOME', UNSET), diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index ff495c74c..6d0f4e4b7 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -18,6 +18,6 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: - out = hook.entry.encode() + b'\n\n' + out = f'{hook.entry}\n\n'.encode() out += b'\n'.join(f.encode() for f in file_args) + b'\n' return 1, out diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 914d87972..595686091 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -68,7 +68,7 @@ def install_environment( # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx?f=255&MSPPError=-2147217396#maxpath if sys.platform == 'win32': # pragma: no cover - envdir = '\\\\?\\' + os.path.normpath(envdir) + envdir = f'\\\\?\\{os.path.normpath(envdir)}' with clean_path_on_failure(envdir): cmd = [ sys.executable, '-mnodeenv', '--prebuilt', '--clean-src', envdir, @@ -83,7 +83,7 @@ def install_environment( helpers.run_setup_cmd(prefix, ('npm', 'install')) helpers.run_setup_cmd( prefix, - ('npm', 'install', '-g', '.') + additional_dependencies, + ('npm', 'install', '-g', '.', *additional_dependencies), ) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index b9078113f..8ccfb66dc 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -49,9 +49,8 @@ def _find_by_py_launcher( if version.startswith('python'): num = version[len('python'):] try: - return cmd_output( - 'py', f'-{num}', '-c', 'import sys; print(sys.executable)', - )[1].strip() + cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)') + return cmd_output(*cmd)[1].strip() except CalledProcessError: pass return None diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index fb3ba9314..0748856e7 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -109,12 +109,14 @@ def install_environment( # Need to call this after installing to set up the shims helpers.run_setup_cmd(prefix, ('rbenv', 'rehash')) helpers.run_setup_cmd( - prefix, ('gem', 'build') + prefix.star('.gemspec'), + prefix, ('gem', 'build', *prefix.star('.gemspec')), ) helpers.run_setup_cmd( prefix, - ('gem', 'install', '--no-document') + - prefix.star('.gem') + additional_dependencies, + ( + 'gem', 'install', '--no-document', + *prefix.star('.gem'), *additional_dependencies, + ), ) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index c570e3c74..159062036 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -27,10 +27,7 @@ def get_env_patch(target_dir: str) -> PatchesT: return ( - ( - 'PATH', - (os.path.join(target_dir, 'bin'), os.pathsep, Var('PATH')), - ), + ('PATH', (os.path.join(target_dir, 'bin'), os.pathsep, Var('PATH'))), ) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 2f7235c9d..7f79719db 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -18,6 +18,5 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: - cmd = hook.cmd - cmd = (hook.prefix.path(cmd[0]),) + cmd[1:] + cmd = (hook.prefix.path(hook.cmd[0]), *hook.cmd[1:]) return helpers.run_xargs(hook, cmd, file_args, color=color) diff --git a/pre_commit/main.py b/pre_commit/main.py index eae4f9096..d96b35fbb 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -329,7 +329,8 @@ def main(argv: Optional[Sequence[str]] = None) -> int: return install( args.config, store, hook_types=args.hook_types, - overwrite=args.overwrite, hooks=args.install_hooks, + overwrite=args.overwrite, + hooks=args.install_hooks, skip_on_missing_config=args.allow_missing_config, ) elif args.command == 'init-templatedir': diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index 5eb1eb7af..c31bcd714 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -34,7 +34,7 @@ def make_archive(name: str, repo: str, ref: str, destdir: str) -> str: :param text ref: Tag/SHA/branch to check out. :param text destdir: Directory to place archives in. """ - output_path = os.path.join(destdir, name + '.tar.gz') + output_path = os.path.join(destdir, f'{name}.tar.gz') with tmpdir() as tempdir: # Clone the repository to the temporary directory cmd_output_b('git', 'clone', repo, tempdir) @@ -56,9 +56,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: parser.add_argument('--dest', default='pre_commit/resources') args = parser.parse_args(argv) for archive_name, repo, ref in REPOS: - output.write_line( - f'Making {archive_name}.tar.gz for {repo}@{ref}', - ) + output.write_line(f'Making {archive_name}.tar.gz for {repo}@{ref}') make_archive(archive_name, repo, ref, args.dest) return 0 diff --git a/pre_commit/output.py b/pre_commit/output.py index b20b8ab4e..24f9d8465 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -4,59 +4,6 @@ from typing import IO from typing import Optional -from pre_commit import color - - -def get_hook_message( - start: str, - postfix: str = '', - end_msg: Optional[str] = None, - end_len: int = 0, - end_color: Optional[str] = None, - use_color: Optional[bool] = None, - cols: int = 80, -) -> str: - """Prints a message for running a hook. - - This currently supports three approaches: - - # Print `start` followed by dots, leaving 6 characters at the end - >>> print_hook_message('start', end_len=6) - start............................................................... - - # Print `start` followed by dots with the end message colored if coloring - # is specified and a newline afterwards - >>> print_hook_message( - 'start', - end_msg='end', - end_color=color.RED, - use_color=True, - ) - start...................................................................end - - # Print `start` followed by dots, followed by the `postfix` message - # uncolored, followed by the `end_msg` colored if specified and a newline - # afterwards - >>> print_hook_message( - 'start', - postfix='postfix ', - end_msg='end', - end_color=color.RED, - use_color=True, - ) - start...........................................................postfix end - """ - if end_len: - assert end_msg is None, end_msg - return start + '.' * (cols - len(start) - end_len - 1) - else: - assert end_msg is not None - assert end_color is not None - assert use_color is not None - dots = '.' * (cols - len(start) - len(postfix) - len(end_msg) - 1) - end = color.format_color(end_msg, end_color, use_color) - return f'{start}{dots}{postfix}{end}\n' - def write(s: str, stream: IO[bytes] = sys.stdout.buffer) -> None: stream.write(s.encode()) diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index c1264da92..128a5c8da 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -20,8 +20,7 @@ def parse_filename(filename: str) -> Tuple[str, ...]: def find_executable( - exe: str, - _environ: Optional[Mapping[str, str]] = None, + exe: str, _environ: Optional[Mapping[str, str]] = None, ) -> Optional[str]: exe = os.path.normpath(exe) if os.sep in exe: diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 9b0710899..1ab9a2a9b 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -32,7 +32,7 @@ def _state(additional_deps: Sequence[str]) -> object: def _state_filename(prefix: Prefix, venv: str) -> str: - return prefix.path(venv, '.install_state_v' + C.INSTALLED_STATE_VERSION) + return prefix.path(venv, f'.install_state_v{C.INSTALLED_STATE_VERSION}') def _read_state(prefix: Prefix, venv: str) -> Optional[object]: @@ -46,7 +46,7 @@ def _read_state(prefix: Prefix, venv: str) -> Optional[object]: def _write_state(prefix: Prefix, venv: str, state: object) -> None: state_filename = _state_filename(prefix, venv) - staging = state_filename + 'staging' + staging = f'{state_filename}staging' with open(staging, 'w') as state_file: state_file.write(json.dumps(state)) # Move the file into place atomically to indicate we've installed diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 22608e59a..09d323dc7 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -50,9 +50,7 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: patch_filename = f'patch{int(time.time())}' patch_filename = os.path.join(patch_dir, patch_filename) logger.warning('Unstaged files detected.') - logger.info( - f'Stashing unstaged files to {patch_filename}.', - ) + logger.info(f'Stashing unstaged files to {patch_filename}.') # Save the current unstaged changes as a patch os.makedirs(patch_dir, exist_ok=True) with open(patch_filename, 'wb') as patch_file: diff --git a/tests/color_test.py b/tests/color_test.py index 50c07d7e0..98b39c1e1 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -6,13 +6,12 @@ from pre_commit import envcontext from pre_commit.color import format_color from pre_commit.color import GREEN -from pre_commit.color import InvalidColorSetting from pre_commit.color import use_color @pytest.mark.parametrize( ('in_text', 'in_color', 'in_use_color', 'expected'), ( - ('foo', GREEN, True, f'{GREEN}foo\033[0m'), + ('foo', GREEN, True, f'{GREEN}foo\033[m'), ('foo', GREEN, False, 'foo'), ), ) @@ -56,5 +55,5 @@ def test_use_color_dumb_term(): def test_use_color_raises_if_given_shenanigans(): - with pytest.raises(InvalidColorSetting): + with pytest.raises(ValueError): use_color('herpaderp') diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index c611bfb62..562293db0 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -34,7 +34,7 @@ def test_is_script(): def test_is_previous_pre_commit(tmpdir): f = tmpdir.join('foo') - f.write(PRIOR_HASHES[0] + '\n') + f.write(f'{PRIOR_HASHES[0]}\n') assert is_our_script(f.strpath) @@ -129,11 +129,11 @@ def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): NORMAL_PRE_COMMIT_RUN = re.compile( - r'^\[INFO\] Initializing environment for .+\.\n' - r'Bash hook\.+Passed\n' - r'\[master [a-f0-9]{7}\] commit!\n' + - FILES_CHANGED + - r' create mode 100644 foo\n$', + fr'^\[INFO\] Initializing environment for .+\.\n' + fr'Bash hook\.+Passed\n' + fr'\[master [a-f0-9]{{7}}\] commit!\n' + fr'{FILES_CHANGED}' + fr' create mode 100644 foo\n$', ) @@ -296,10 +296,10 @@ def test_failing_hooks_returns_nonzero(tempdir_factory, store): EXISTING_COMMIT_RUN = re.compile( - r'^legacy hook\n' - r'\[master [a-f0-9]{7}\] commit!\n' + - FILES_CHANGED + - r' create mode 100644 baz\n$', + fr'^legacy hook\n' + fr'\[master [a-f0-9]{{7}}\] commit!\n' + fr'{FILES_CHANGED}' + fr' create mode 100644 baz\n$', ) @@ -453,10 +453,10 @@ def test_uninstall_doesnt_remove_not_our_hooks(in_git_dir): PRE_INSTALLED = re.compile( - r'Bash hook\.+Passed\n' - r'\[master [a-f0-9]{7}\] commit!\n' + - FILES_CHANGED + - r' create mode 100644 foo\n$', + fr'Bash hook\.+Passed\n' + fr'\[master [a-f0-9]{{7}}\] commit!\n' + fr'{FILES_CHANGED}' + fr' create mode 100644 foo\n$', ) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 1ed866bcd..d2e2f2360 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -7,10 +7,13 @@ import pytest import pre_commit.constants as C +from pre_commit import color from pre_commit.commands.install_uninstall import install from pre_commit.commands.run import _compute_cols +from pre_commit.commands.run import _full_msg from pre_commit.commands.run import _get_skips from pre_commit.commands.run import _has_unmerged_paths +from pre_commit.commands.run import _start_msg from pre_commit.commands.run import Classifier from pre_commit.commands.run import filter_by_include_exclude from pre_commit.commands.run import run @@ -29,6 +32,62 @@ from testing.util import run_opts +def test_start_msg(): + ret = _start_msg(start='start', end_len=5, cols=15) + # 4 dots: 15 - 5 - 5 - 1 + assert ret == 'start....' + + +def test_full_msg(): + ret = _full_msg( + start='start', + end_msg='end', + end_color='', + use_color=False, + cols=15, + ) + # 6 dots: 15 - 5 - 3 - 1 + assert ret == 'start......end\n' + + +def test_full_msg_with_color(): + ret = _full_msg( + start='start', + end_msg='end', + end_color=color.RED, + use_color=True, + cols=15, + ) + # 6 dots: 15 - 5 - 3 - 1 + assert ret == f'start......{color.RED}end{color.NORMAL}\n' + + +def test_full_msg_with_postfix(): + ret = _full_msg( + start='start', + postfix='post ', + end_msg='end', + end_color='', + use_color=False, + cols=20, + ) + # 6 dots: 20 - 5 - 5 - 3 - 1 + assert ret == 'start......post end\n' + + +def test_full_msg_postfix_not_colored(): + ret = _full_msg( + start='start', + postfix='post ', + end_msg='end', + end_color=color.RED, + use_color=True, + cols=20, + ) + # 6 dots: 20 - 5 - 5 - 3 - 1 + assert ret == f'start......post {color.RED}end{color.NORMAL}\n' + + @pytest.fixture def repo_with_passing_hook(tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') @@ -173,7 +232,7 @@ def test_global_exclude(cap_out, store, in_git_dir): ret, printed = _do_run(cap_out, store, str(in_git_dir), opts) assert ret == 0 # Does not contain foo.py since it was excluded - assert printed.startswith(b'identity' + b'.' * 65 + b'Passed\n') + assert printed.startswith(f'identity{"." * 65}Passed\n'.encode()) assert printed.endswith(b'\n\n.pre-commit-config.yaml\nbar.py\n\n') @@ -190,7 +249,7 @@ def test_global_files(cap_out, store, in_git_dir): ret, printed = _do_run(cap_out, store, str(in_git_dir), opts) assert ret == 0 # Does not contain foo.py since it was excluded - assert printed.startswith(b'identity' + b'.' * 65 + b'Passed\n') + assert printed.startswith(f'identity{"." * 65}Passed\n'.encode()) assert printed.endswith(b'\n\nbar.py\n\n') diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index fca0f3dd1..d3ec3fda2 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -21,7 +21,7 @@ def try_repo_opts(repo, ref=None, **kwargs): def _get_out(cap_out): out = re.sub(r'\[INFO\].+\n', '', cap_out.get()) - start, using_config, config, rest = out.split('=' * 79 + '\n') + start, using_config, config, rest = out.split(f'{"=" * 79}\n') assert using_config == 'Using config:\n' return start, config, rest diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 8fa41a704..a8626f73f 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -140,7 +140,6 @@ def test_error_handler_no_tty(tempdir_factory): ret, out, _ = cmd_output_mocked_pre_commit_home( sys.executable, '-c', - 'from __future__ import unicode_literals\n' 'from pre_commit.error_handler import error_handler\n' 'with error_handler():\n' ' raise ValueError("\\u2603")\n', diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index da48e3323..19890d746 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -16,7 +16,7 @@ def test_norm_version_expanduser(): expected_path = fr'{home}\python343' else: # pragma: windows no cover path = '~/.pyenv/versions/3.4.3/bin/python' - expected_path = home + '/.pyenv/versions/3.4.3/bin/python' + expected_path = f'{home}/.pyenv/versions/3.4.3/bin/python' result = python.norm_version(path) assert result == expected_path diff --git a/tests/logging_handler_test.py b/tests/logging_handler_test.py index e1506d495..fe68593b9 100644 --- a/tests/logging_handler_test.py +++ b/tests/logging_handler_test.py @@ -12,7 +12,7 @@ def test_logging_handler_color(cap_out): handler = LoggingHandler(True) handler.emit(_log_record('hi', logging.WARNING)) ret = cap_out.get() - assert ret == color.YELLOW + '[WARNING]' + color.NORMAL + ' hi\n' + assert ret == f'{color.YELLOW}[WARNING]{color.NORMAL} hi\n' def test_logging_handler_no_color(cap_out): diff --git a/tests/output_test.py b/tests/output_test.py index e56c5b74b..1cdacbbce 100644 --- a/tests/output_test.py +++ b/tests/output_test.py @@ -1,85 +1,9 @@ -from unittest import mock +import io -import pytest - -from pre_commit import color from pre_commit import output -@pytest.mark.parametrize( - 'kwargs', - ( - # both end_msg and end_len - {'end_msg': 'end', 'end_len': 1, 'end_color': '', 'use_color': True}, - # Neither end_msg nor end_len - {}, - # Neither color option for end_msg - {'end_msg': 'end'}, - # No use_color for end_msg - {'end_msg': 'end', 'end_color': ''}, - # No end_color for end_msg - {'end_msg': 'end', 'use_color': ''}, - ), -) -def test_get_hook_message_raises(kwargs): - with pytest.raises(AssertionError): - output.get_hook_message('start', **kwargs) - - -def test_case_with_end_len(): - ret = output.get_hook_message('start', end_len=5, cols=15) - assert ret == 'start' + '.' * 4 - - -def test_case_with_end_msg(): - ret = output.get_hook_message( - 'start', - end_msg='end', - end_color='', - use_color=False, - cols=15, - ) - assert ret == 'start' + '.' * 6 + 'end' + '\n' - - -def test_case_with_end_msg_using_color(): - ret = output.get_hook_message( - 'start', - end_msg='end', - end_color=color.RED, - use_color=True, - cols=15, - ) - assert ret == 'start' + '.' * 6 + color.RED + 'end' + color.NORMAL + '\n' - - -def test_case_with_postfix_message(): - ret = output.get_hook_message( - 'start', - postfix='post ', - end_msg='end', - end_color='', - use_color=False, - cols=20, - ) - assert ret == 'start' + '.' * 6 + 'post ' + 'end' + '\n' - - -def test_make_sure_postfix_is_not_colored(): - ret = output.get_hook_message( - 'start', - postfix='post ', - end_msg='end', - end_color=color.RED, - use_color=True, - cols=20, - ) - assert ret == ( - 'start' + '.' * 6 + 'post ' + color.RED + 'end' + color.NORMAL + '\n' - ) - - def test_output_write_writes(): - fake_stream = mock.Mock() - output.write('hello world', fake_stream) - assert fake_stream.write.call_count == 1 + stream = io.BytesIO() + output.write('hello world', stream) + assert stream.getvalue() == b'hello world' diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index be9de3953..ddb957435 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -94,9 +94,9 @@ def test_foo_something_unstaged_diff_color_always(foo_staged, patch_dir): def test_foo_both_modify_non_conflicting(foo_staged, patch_dir): with open(foo_staged.foo_filename, 'w') as foo_file: - foo_file.write(FOO_CONTENTS + '9\n') + foo_file.write(f'{FOO_CONTENTS}9\n') - _test_foo_state(foo_staged, FOO_CONTENTS + '9\n', 'AM') + _test_foo_state(foo_staged, f'{FOO_CONTENTS}9\n', 'AM') with staged_files_only(patch_dir): _test_foo_state(foo_staged) @@ -107,7 +107,7 @@ def test_foo_both_modify_non_conflicting(foo_staged, patch_dir): _test_foo_state(foo_staged, FOO_CONTENTS.replace('1', 'a'), 'AM') - _test_foo_state(foo_staged, FOO_CONTENTS.replace('1', 'a') + '9\n', 'AM') + _test_foo_state(foo_staged, f'{FOO_CONTENTS.replace("1", "a")}9\n', 'AM') def test_foo_both_modify_conflicting(foo_staged, patch_dir): From 755b8000f653a34277915d4c8a6e6eb76fd6abea Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 14 Jan 2020 16:07:50 -0800 Subject: [PATCH 536/544] move Hook data type to a separate file --- pre_commit/commands/run.py | 6 +- pre_commit/hook.py | 63 +++++++++++++++ pre_commit/languages/all.py | 5 +- pre_commit/languages/conda.py | 5 +- pre_commit/languages/docker.py | 5 +- pre_commit/languages/docker_image.py | 5 +- pre_commit/languages/fail.py | 5 +- pre_commit/languages/golang.py | 5 +- pre_commit/languages/helpers.py | 5 +- pre_commit/languages/node.py | 5 +- pre_commit/languages/pygrep.py | 5 +- pre_commit/languages/python.py | 5 +- pre_commit/languages/ruby.py | 5 +- pre_commit/languages/rust.py | 5 +- pre_commit/languages/script.py | 5 +- pre_commit/languages/swift.py | 5 +- pre_commit/languages/system.py | 5 +- pre_commit/repository.py | 116 +++++++-------------------- tests/repository_test.py | 42 ++++++---- 19 files changed, 139 insertions(+), 163 deletions(-) create mode 100644 pre_commit/hook.py diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index ce5a06c2e..6690bdd4e 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -20,8 +20,9 @@ from pre_commit import git from pre_commit import output from pre_commit.clientlib import load_config +from pre_commit.hook import Hook +from pre_commit.languages.all import languages from pre_commit.repository import all_hooks -from pre_commit.repository import Hook from pre_commit.repository import install_hook_envs from pre_commit.staged_files_only import staged_files_only from pre_commit.store import Store @@ -160,7 +161,8 @@ def _run_single_hook( if not hook.pass_filenames: filenames = () time_before = time.time() - retcode, out = hook.run(filenames, use_color) + language = languages[hook.language] + retcode, out = language.run_hook(hook, filenames, use_color) duration = round(time.time() - time_before, 2) or 0 diff_after = cmd_output_b(*diff_cmd, retcode=None) diff --git a/pre_commit/hook.py b/pre_commit/hook.py new file mode 100644 index 000000000..b65ac42b0 --- /dev/null +++ b/pre_commit/hook.py @@ -0,0 +1,63 @@ +import logging +import shlex +from typing import Any +from typing import Dict +from typing import NamedTuple +from typing import Sequence +from typing import Tuple + +from pre_commit.prefix import Prefix + +logger = logging.getLogger('pre_commit') + + +class Hook(NamedTuple): + src: str + prefix: Prefix + id: str + name: str + entry: str + language: str + alias: str + files: str + exclude: str + types: Sequence[str] + exclude_types: Sequence[str] + additional_dependencies: Sequence[str] + args: Sequence[str] + always_run: bool + pass_filenames: bool + description: str + language_version: str + log_file: str + minimum_pre_commit_version: str + require_serial: bool + stages: Sequence[str] + verbose: bool + + @property + def cmd(self) -> Tuple[str, ...]: + return (*shlex.split(self.entry), *self.args) + + @property + def install_key(self) -> Tuple[Prefix, str, str, Tuple[str, ...]]: + return ( + self.prefix, + self.language, + self.language_version, + tuple(self.additional_dependencies), + ) + + @classmethod + def create(cls, src: str, prefix: Prefix, dct: Dict[str, Any]) -> 'Hook': + # TODO: have cfgv do this (?) + extra_keys = set(dct) - _KEYS + if extra_keys: + logger.warning( + f'Unexpected key(s) present on {src} => {dct["id"]}: ' + f'{", ".join(sorted(extra_keys))}', + ) + return cls(src=src, prefix=prefix, **{k: dct[k] for k in _KEYS}) + + +_KEYS = frozenset(set(Hook._fields) - {'src', 'prefix'}) diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 28f44af40..e6d7b1dbc 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -3,8 +3,8 @@ from typing import Optional from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING +from pre_commit.hook import Hook from pre_commit.languages import conda from pre_commit.languages import docker from pre_commit.languages import docker_image @@ -21,9 +21,6 @@ from pre_commit.languages import system from pre_commit.prefix import Prefix -if TYPE_CHECKING: - from pre_commit.repository import Hook - class Language(NamedTuple): name: str diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index 117a44a46..2c187e02f 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -3,21 +3,18 @@ from typing import Generator from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import SubstitutionT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var +from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b -if TYPE_CHECKING: - from pre_commit.repository import Hook - ENVIRONMENT_DIR = 'conda' get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 00090f118..364a69967 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -2,18 +2,15 @@ import os from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING import pre_commit.constants as C +from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b -if TYPE_CHECKING: - from pre_commit.repository import Hook - ENVIRONMENT_DIR = 'docker' PRE_COMMIT_LABEL = 'PRE_COMMIT' get_default_version = helpers.basic_get_default_version diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index 0bf00e7d8..58da34c13 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -1,14 +1,11 @@ from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING +from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.languages.docker import assert_docker_available from pre_commit.languages.docker import docker_cmd -if TYPE_CHECKING: - from pre_commit.repository import Hook - ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index 6d0f4e4b7..8cdc76c95 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -1,12 +1,9 @@ from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING +from pre_commit.hook import Hook from pre_commit.languages import helpers -if TYPE_CHECKING: - from pre_commit.repository import Hook - ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 9d50e6352..cdcff0d58 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -4,13 +4,13 @@ from typing import Generator from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING import pre_commit.constants as C from pre_commit import git from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var +from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure @@ -18,9 +18,6 @@ from pre_commit.util import cmd_output_b from pre_commit.util import rmtree -if TYPE_CHECKING: - from pre_commit.repository import Hook - ENVIRONMENT_DIR = 'golangenv' get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 3a9d4d6d5..3b5382912 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -8,16 +8,13 @@ from typing import overload from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING import pre_commit.constants as C +from pre_commit.hook import Hook from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b from pre_commit.xargs import xargs -if TYPE_CHECKING: - from pre_commit.repository import Hook - FIXED_RANDOM_SEED = 1542676186 diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 595686091..481b0655f 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -4,12 +4,12 @@ from typing import Generator from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var +from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.languages.python import bin_dir from pre_commit.prefix import Prefix @@ -17,9 +17,6 @@ from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b -if TYPE_CHECKING: - from pre_commit.repository import Hook - ENVIRONMENT_DIR = 'node_env' get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index c6d1131df..68eb6e9be 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -5,15 +5,12 @@ from typing import Pattern from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING from pre_commit import output +from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.xargs import xargs -if TYPE_CHECKING: - from pre_commit.repository import Hook - ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 8ccfb66dc..1def27b0f 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -8,13 +8,13 @@ from typing import Optional from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var +from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.parse_shebang import find_executable from pre_commit.prefix import Prefix @@ -23,9 +23,6 @@ from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b -if TYPE_CHECKING: - from pre_commit.repository import Hook - ENVIRONMENT_DIR = 'py_env' diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 0748856e7..828216fe1 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -5,21 +5,18 @@ from typing import Generator from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var +from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import resource_bytesio -if TYPE_CHECKING: - from pre_comit.repository import Hook - ENVIRONMENT_DIR = 'rbenv' get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 159062036..feb36847b 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -4,7 +4,6 @@ from typing import Sequence from typing import Set from typing import Tuple -from typing import TYPE_CHECKING import toml @@ -12,14 +11,12 @@ from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var +from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b -if TYPE_CHECKING: - from pre_commit.repository import Hook - ENVIRONMENT_DIR = 'rustenv' get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 7f79719db..1f6f354d5 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -1,12 +1,9 @@ from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING +from pre_commit.hook import Hook from pre_commit.languages import helpers -if TYPE_CHECKING: - from pre_commit.repository import Hook - ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 28e88f374..9f36b1521 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -3,20 +3,17 @@ from typing import Generator from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var +from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b -if TYPE_CHECKING: - from pre_commit.repository import Hook - ENVIRONMENT_DIR = 'swift_env' get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index a920f736f..424e14fc4 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -1,12 +1,9 @@ from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING +from pre_commit.hook import Hook from pre_commit.languages import helpers -if TYPE_CHECKING: - from pre_commit.repository import Hook - ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 1ab9a2a9b..77734ee64 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -1,11 +1,9 @@ import json import logging import os -import shlex from typing import Any from typing import Dict from typing import List -from typing import NamedTuple from typing import Optional from typing import Sequence from typing import Set @@ -14,8 +12,8 @@ import pre_commit.constants as C from pre_commit.clientlib import load_manifest from pre_commit.clientlib import LOCAL -from pre_commit.clientlib import MANIFEST_HOOK_DICT from pre_commit.clientlib import META +from pre_commit.hook import Hook from pre_commit.languages.all import languages from pre_commit.languages.helpers import environment_dir from pre_commit.prefix import Prefix @@ -53,93 +51,39 @@ def _write_state(prefix: Prefix, venv: str, state: object) -> None: os.rename(staging, state_filename) -_KEYS = tuple(item.key for item in MANIFEST_HOOK_DICT.items) - - -class Hook(NamedTuple): - src: str - prefix: Prefix - id: str - name: str - entry: str - language: str - alias: str - files: str - exclude: str - types: Sequence[str] - exclude_types: Sequence[str] - additional_dependencies: Sequence[str] - args: Sequence[str] - always_run: bool - pass_filenames: bool - description: str - language_version: str - log_file: str - minimum_pre_commit_version: str - require_serial: bool - stages: Sequence[str] - verbose: bool - - @property - def cmd(self) -> Tuple[str, ...]: - return tuple(shlex.split(self.entry)) + tuple(self.args) - - @property - def install_key(self) -> Tuple[Prefix, str, str, Tuple[str, ...]]: - return ( - self.prefix, - self.language, - self.language_version, - tuple(self.additional_dependencies), +def _hook_installed(hook: Hook) -> bool: + lang = languages[hook.language] + venv = environment_dir(lang.ENVIRONMENT_DIR, hook.language_version) + return ( + venv is None or ( + ( + _read_state(hook.prefix, venv) == + _state(hook.additional_dependencies) + ) and + lang.healthy(hook.prefix, hook.language_version) ) + ) - def installed(self) -> bool: - lang = languages[self.language] - venv = environment_dir(lang.ENVIRONMENT_DIR, self.language_version) - return ( - venv is None or ( - ( - _read_state(self.prefix, venv) == - _state(self.additional_dependencies) - ) and - lang.healthy(self.prefix, self.language_version) - ) - ) - def install(self) -> None: - logger.info(f'Installing environment for {self.src}.') - logger.info('Once installed this environment will be reused.') - logger.info('This may take a few minutes...') +def _hook_install(hook: Hook) -> None: + logger.info(f'Installing environment for {hook.src}.') + logger.info('Once installed this environment will be reused.') + logger.info('This may take a few minutes...') - lang = languages[self.language] - assert lang.ENVIRONMENT_DIR is not None - venv = environment_dir(lang.ENVIRONMENT_DIR, self.language_version) + lang = languages[hook.language] + assert lang.ENVIRONMENT_DIR is not None + venv = environment_dir(lang.ENVIRONMENT_DIR, hook.language_version) - # There's potentially incomplete cleanup from previous runs - # Clean it up! - if self.prefix.exists(venv): - rmtree(self.prefix.path(venv)) + # There's potentially incomplete cleanup from previous runs + # Clean it up! + if hook.prefix.exists(venv): + rmtree(hook.prefix.path(venv)) - lang.install_environment( - self.prefix, self.language_version, self.additional_dependencies, - ) - # Write our state to indicate we're installed - _write_state(self.prefix, venv, _state(self.additional_dependencies)) - - def run(self, file_args: Sequence[str], color: bool) -> Tuple[int, bytes]: - lang = languages[self.language] - return lang.run_hook(self, file_args, color) - - @classmethod - def create(cls, src: str, prefix: Prefix, dct: Dict[str, Any]) -> 'Hook': - # TODO: have cfgv do this (?) - extra_keys = set(dct) - set(_KEYS) - if extra_keys: - logger.warning( - f'Unexpected key(s) present on {src} => {dct["id"]}: ' - f'{", ".join(sorted(extra_keys))}', - ) - return cls(src=src, prefix=prefix, **{k: dct[k] for k in _KEYS}) + lang.install_environment( + hook.prefix, hook.language_version, hook.additional_dependencies, + ) + # Write our state to indicate we're installed + _write_state(hook.prefix, venv, _state(hook.additional_dependencies)) def _hook( @@ -243,7 +187,7 @@ def _need_installed() -> List[Hook]: seen: Set[Tuple[Prefix, str, str, Tuple[str, ...]]] = set() ret = [] for hook in hooks: - if hook.install_key not in seen and not hook.installed(): + if hook.install_key not in seen and not _hook_installed(hook): ret.append(hook) seen.add(hook.install_key) return ret @@ -253,7 +197,7 @@ def _need_installed() -> List[Hook]: with store.exclusive_lock(): # Another process may have already completed this work for hook in _need_installed(): - hook.install() + _hook_install(hook) def all_hooks(root_config: Dict[str, Any], store: Store) -> Tuple[Hook, ...]: diff --git a/tests/repository_test.py b/tests/repository_test.py index 2dc9e8665..21f2f41ce 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -13,15 +13,16 @@ from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest from pre_commit.envcontext import envcontext +from pre_commit.hook import Hook from pre_commit.languages import golang from pre_commit.languages import helpers from pre_commit.languages import node from pre_commit.languages import python from pre_commit.languages import ruby from pre_commit.languages import rust +from pre_commit.languages.all import languages from pre_commit.prefix import Prefix from pre_commit.repository import all_hooks -from pre_commit.repository import Hook from pre_commit.repository import install_hook_envs from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b @@ -40,6 +41,10 @@ def _norm_out(b): return b.replace(b'\r\n', b'\n') +def _hook_run(hook, filenames, color): + return languages[hook.language].run_hook(hook, filenames, color) + + def _get_hook_no_install(repo_config, store, hook_id): config = {'repos': [repo_config]} config = cfgv.validate(config, CONFIG_SCHEMA) @@ -68,7 +73,8 @@ def _test_hook_repo( ): path = make_repo(tempdir_factory, repo_path) config = make_config_from_repo(path, **(config_kwargs or {})) - ret, out = _get_hook(config, store, hook_id).run(args, color=color) + hook = _get_hook(config, store, hook_id) + ret, out = _hook_run(hook, args, color=color) assert ret == expected_return_code assert _norm_out(out) == expected @@ -108,7 +114,8 @@ def test_local_conda_additional_dependencies(store): 'additional_dependencies': ['mccabe'], }], } - ret, out = _get_hook(config, store, 'local-conda').run((), color=False) + hook = _get_hook(config, store, 'local-conda') + ret, out = _hook_run(hook, (), color=False) assert ret == 0 assert _norm_out(out) == b'OK\n' @@ -173,7 +180,7 @@ def run_on_version(version, expected_output): config = make_config_from_repo(path) config['hooks'][0]['language_version'] = version hook = _get_hook(config, store, 'python3-hook') - ret, out = hook.run([], color=False) + ret, out = _hook_run(hook, [], color=False) assert ret == 0 assert _norm_out(out) == expected_output @@ -445,14 +452,14 @@ def greppable_files(tmpdir): def test_grep_hook_matching(greppable_files, store): hook = _make_grep_repo('ello', store) - ret, out = hook.run(('f1', 'f2', 'f3'), color=False) + ret, out = _hook_run(hook, ('f1', 'f2', 'f3'), color=False) assert ret == 1 assert _norm_out(out) == b"f1:1:hello'hi\n" def test_grep_hook_case_insensitive(greppable_files, store): hook = _make_grep_repo('ELLO', store, args=['-i']) - ret, out = hook.run(('f1', 'f2', 'f3'), color=False) + ret, out = _hook_run(hook, ('f1', 'f2', 'f3'), color=False) assert ret == 1 assert _norm_out(out) == b"f1:1:hello'hi\n" @@ -460,7 +467,7 @@ def test_grep_hook_case_insensitive(greppable_files, store): @pytest.mark.parametrize('regex', ('nope', "foo'bar", r'^\[INFO\]')) def test_grep_hook_not_matching(regex, greppable_files, store): hook = _make_grep_repo(regex, store) - ret, out = hook.run(('f1', 'f2', 'f3'), color=False) + ret, out = _hook_run(hook, ('f1', 'f2', 'f3'), color=False) assert (ret, out) == (0, b'') @@ -559,7 +566,8 @@ def test_local_golang_additional_dependencies(store): 'additional_dependencies': ['github.com/golang/example/hello'], }], } - ret, out = _get_hook(config, store, 'hello').run((), color=False) + hook = _get_hook(config, store, 'hello') + ret, out = _hook_run(hook, (), color=False) assert ret == 0 assert _norm_out(out) == b'Hello, Go examples!\n' @@ -575,7 +583,8 @@ def test_local_rust_additional_dependencies(store): 'additional_dependencies': ['cli:hello-cli:0.2.2'], }], } - ret, out = _get_hook(config, store, 'hello').run((), color=False) + hook = _get_hook(config, store, 'hello') + ret, out = _hook_run(hook, (), color=False) assert ret == 0 assert _norm_out(out) == b'Hello World!\n' @@ -592,7 +601,9 @@ def test_fail_hooks(store): }], } hook = _get_hook(config, store, 'fail') - ret, out = hook.run(('changelog/123.bugfix', 'changelog/wat'), color=False) + ret, out = _hook_run( + hook, ('changelog/123.bugfix', 'changelog/wat'), color=False, + ) assert ret == 1 assert out == ( b'make sure to name changelogs as .rst!\n' @@ -661,7 +672,7 @@ class MyKeyboardInterrupt(KeyboardInterrupt): # However, it should be perfectly runnable (reinstall after botched # install) install_hook_envs(hooks, store) - ret, out = hook.run((), color=False) + ret, out = _hook_run(hook, (), color=False) assert ret == 0 @@ -683,7 +694,8 @@ def test_invalidated_virtualenv(tempdir_factory, store): cmd_output_b('rm', '-rf', *paths) # pre-commit should rebuild the virtualenv and it should be runnable - ret, out = _get_hook(config, store, 'foo').run((), color=False) + hook = _get_hook(config, store, 'foo') + ret, out = _hook_run(hook, (), color=False) assert ret == 0 @@ -724,13 +736,13 @@ def test_tags_on_repositories(in_tmpdir, tempdir_factory, store): config1 = make_config_from_repo(git1, rev=tag) hook1 = _get_hook(config1, store, 'prints_cwd') - ret1, out1 = hook1.run(('-L',), color=False) + ret1, out1 = _hook_run(hook1, ('-L',), color=False) assert ret1 == 0 assert out1.strip() == _norm_pwd(in_tmpdir) config2 = make_config_from_repo(git2, rev=tag) hook2 = _get_hook(config2, store, 'bash_hook') - ret2, out2 = hook2.run(('bar',), color=False) + ret2, out2 = _hook_run(hook2, ('bar',), color=False) assert ret2 == 0 assert out2 == b'bar\nHello World\n' @@ -754,7 +766,7 @@ def test_local_python_repo(store, local_python_config): hook = _get_hook(local_python_config, store, 'foo') # language_version should have been adjusted to the interpreter version assert hook.language_version != C.DEFAULT - ret, out = hook.run(('filename',), color=False) + ret, out = _hook_run(hook, ('filename',), color=False) assert ret == 0 assert _norm_out(out) == b"['filename']\nHello World\n" From 2f51b9da1c526ee6ed6a317d2dfd259d0072dbae Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 16 Jan 2020 09:57:41 -0800 Subject: [PATCH 537/544] Use a more specific hook shebang now that it can't be python 2 --- pre_commit/commands/install_uninstall.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 7aeba2286..a9c46d90b 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -56,8 +56,8 @@ def shebang() -> str: # Homebrew/homebrew-core#35825: be more timid about appropriate `PATH` path_choices = [p for p in os.defpath.split(os.pathsep) if p] exe_choices = [ - 'python{}'.format('.'.join(str(v) for v in sys.version_info[:i])) - for i in range(3) + f'python{sys.version_info[0]}.{sys.version_info[1]}', + f'python{sys.version_info[0]}', ] for path, exe in itertools.product(path_choices, exe_choices): if os.path.exists(os.path.join(path, exe)): From 57cc814b8ba56ff067803da82ec62a1521a62eed Mon Sep 17 00:00:00 2001 From: David Martinez Barreiro Date: Thu, 16 Jan 2020 18:01:26 +0100 Subject: [PATCH 538/544] Push remote env var details --- pre_commit/commands/run.py | 4 ++++ pre_commit/main.py | 6 ++++++ pre_commit/resources/hook-tmpl | 10 +++++++--- testing/util.py | 4 ++++ tests/commands/run_test.py | 29 +++++++++++++++++++++++++++++ 5 files changed, 50 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 6690bdd4e..89a5bef6c 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -312,6 +312,10 @@ def run( environ['PRE_COMMIT_ORIGIN'] = args.origin environ['PRE_COMMIT_SOURCE'] = args.source + if args.push_remote_name and args.push_remote_url: + environ['PRE_COMMIT_REMOTE_NAME'] = args.push_remote_name + environ['PRE_COMMIT_REMOTE_URL'] = args.push_remote_url + with contextlib.ExitStack() as exit_stack: if stash: exit_stack.enter_context(staged_files_only(store.directory)) diff --git a/pre_commit/main.py b/pre_commit/main.py index d96b35fbb..ac2f41669 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -101,6 +101,12 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: '--commit-msg-filename', help='Filename to check when running during `commit-msg`', ) + parser.add_argument( + '--push-remote-name', help='Remote name used by `git push`.', + ) + parser.add_argument( + '--push-remote-url', help='Remote url used by `git push`.', + ) parser.add_argument( '--hook-stage', choices=C.STAGES, default='commit', help='The stage during which the hook is fired. One of %(choices)s', diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 213d16eef..b405aad42 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -120,7 +120,8 @@ def _rev_exists(rev: str) -> bool: def _pre_push(stdin: bytes) -> Tuple[str, ...]: - remote = sys.argv[1] + remote_name = sys.argv[1] + remote_url = sys.argv[2] opts: Tuple[str, ...] = () for line in stdin.decode().splitlines(): @@ -133,7 +134,7 @@ def _pre_push(stdin: bytes) -> Tuple[str, ...]: # ancestors not found in remote ancestors = subprocess.check_output(( 'git', 'rev-list', local_sha, '--topo-order', '--reverse', - '--not', f'--remotes={remote}', + '--not', f'--remotes={remote_name}', )).decode().strip() if not ancestors: continue @@ -150,7 +151,10 @@ def _pre_push(stdin: bytes) -> Tuple[str, ...]: opts = ('--origin', local_sha, '--source', source) if opts: - return opts + remote_opts = ( + '--push-remote-name', remote_name, '--push-remote-url', remote_url, + ) + return opts + remote_opts else: # An attempt to push an empty changeset raise EarlyExit() diff --git a/testing/util.py b/testing/util.py index 0c2cc6a89..f5caa5e30 100644 --- a/testing/util.py +++ b/testing/util.py @@ -67,6 +67,8 @@ def run_opts( hook=None, origin='', source='', + push_remote_name='', + push_remote_url='', hook_stage='commit', show_diff_on_failure=False, commit_msg_filename='', @@ -81,6 +83,8 @@ def run_opts( hook=hook, origin=origin, source=source, + push_remote_name=push_remote_name, + push_remote_url=push_remote_url, hook_stage=hook_stage, show_diff_on_failure=show_diff_on_failure, commit_msg_filename=commit_msg_filename, diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index d2e2f2360..e56f53908 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -687,6 +687,35 @@ def _run_for_stage(stage): assert _run_for_stage('commit-msg').startswith(b'hook 5...') +def test_push_remote_environment(cap_out, store, repo_with_passing_hook): + config = { + 'repo': 'local', + 'hooks': [ + { + 'id': 'print-push-remote', + 'name': 'Print push remote name', + 'entry': 'entry: bash -c \'echo "$PRE_COMMIT_REMOTE_NAME"\'', + 'language': 'system', + 'verbose': bool(1), + }, + ], + } + add_config_to_repo(repo_with_passing_hook, config) + + _test_run( + cap_out, + store, + repo_with_passing_hook, + opts={ + 'push_remote_name': 'origin', + 'push_remote_url': 'https://github.com/pre-commit/pre-commit', + }, + expected_outputs=[b'Print push remote name', b'Passed'], + expected_ret=0, + stage=['push'], + ) + + def test_commit_msg_hook(cap_out, store, commit_msg_repo): filename = '.git/COMMIT_EDITMSG' with open(filename, 'w') as f: From 0bb8a8fabe9d9ac46266039fd164509c21e53cf5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 16 Jan 2020 09:55:46 -0800 Subject: [PATCH 539/544] Move test to install_uninstall test so environment variables apply --- pre_commit/commands/run.py | 6 ++-- pre_commit/main.py | 6 ++-- pre_commit/resources/hook-tmpl | 5 ++-- testing/util.py | 8 +++--- tests/commands/install_uninstall_test.py | 32 +++++++++++++++++++-- tests/commands/run_test.py | 36 ++++-------------------- 6 files changed, 46 insertions(+), 47 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 89a5bef6c..95f8ab419 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -312,9 +312,9 @@ def run( environ['PRE_COMMIT_ORIGIN'] = args.origin environ['PRE_COMMIT_SOURCE'] = args.source - if args.push_remote_name and args.push_remote_url: - environ['PRE_COMMIT_REMOTE_NAME'] = args.push_remote_name - environ['PRE_COMMIT_REMOTE_URL'] = args.push_remote_url + if args.remote_name and args.remote_url: + environ['PRE_COMMIT_REMOTE_NAME'] = args.remote_name + environ['PRE_COMMIT_REMOTE_URL'] = args.remote_url with contextlib.ExitStack() as exit_stack: if stash: diff --git a/pre_commit/main.py b/pre_commit/main.py index ac2f41669..e65d8ae8a 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -102,11 +102,9 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: help='Filename to check when running during `commit-msg`', ) parser.add_argument( - '--push-remote-name', help='Remote name used by `git push`.', - ) - parser.add_argument( - '--push-remote-url', help='Remote url used by `git push`.', + '--remote-name', help='Remote name used by `git push`.', ) + parser.add_argument('--remote-url', help='Remote url used by `git push`.') parser.add_argument( '--hook-stage', choices=C.STAGES, default='commit', help='The stage during which the hook is fired. One of %(choices)s', diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index b405aad42..573335a96 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -151,10 +151,9 @@ def _pre_push(stdin: bytes) -> Tuple[str, ...]: opts = ('--origin', local_sha, '--source', source) if opts: - remote_opts = ( - '--push-remote-name', remote_name, '--push-remote-url', remote_url, + return ( + *opts, '--remote-name', remote_name, '--remote-url', remote_url, ) - return opts + remote_opts else: # An attempt to push an empty changeset raise EarlyExit() diff --git a/testing/util.py b/testing/util.py index f5caa5e30..ce3206eb8 100644 --- a/testing/util.py +++ b/testing/util.py @@ -67,8 +67,8 @@ def run_opts( hook=None, origin='', source='', - push_remote_name='', - push_remote_url='', + remote_name='', + remote_url='', hook_stage='commit', show_diff_on_failure=False, commit_msg_filename='', @@ -83,8 +83,8 @@ def run_opts( hook=hook, origin=origin, source=source, - push_remote_name=push_remote_name, - push_remote_url=push_remote_url, + remote_name=remote_name, + remote_url=remote_url, hook_stage=hook_stage, show_diff_on_failure=show_diff_on_failure, commit_msg_filename=commit_msg_filename, diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 562293db0..984ae74af 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -15,6 +15,7 @@ from pre_commit.util import cmd_output from pre_commit.util import make_executable from pre_commit.util import resource_text +from testing.fixtures import add_config_to_repo from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo from testing.fixtures import remove_config_from_repo @@ -512,9 +513,9 @@ def test_installed_from_venv(tempdir_factory, store): assert NORMAL_PRE_COMMIT_RUN.match(output) -def _get_push_output(tempdir_factory, opts=()): +def _get_push_output(tempdir_factory, remote='origin', opts=()): return cmd_output_mocked_pre_commit_home( - 'git', 'push', 'origin', 'HEAD:new_branch', *opts, + 'git', 'push', remote, 'HEAD:new_branch', *opts, tempdir_factory=tempdir_factory, retcode=None, )[:2] @@ -589,6 +590,33 @@ def test_pre_push_new_upstream(tempdir_factory, store): assert 'Passed' in output +def test_pre_push_environment_variables(tempdir_factory, store): + config = { + 'repo': 'local', + 'hooks': [ + { + 'id': 'print-remote-info', + 'name': 'print remote info', + 'entry': 'bash -c "echo remote: $PRE_COMMIT_REMOTE_NAME"', + 'language': 'system', + 'verbose': True, + }, + ], + } + + upstream = git_dir(tempdir_factory) + clone = tempdir_factory.get() + cmd_output('git', 'clone', upstream, clone) + add_config_to_repo(clone, config) + with cwd(clone): + install(C.CONFIG_FILE, store, hook_types=['pre-push']) + + cmd_output('git', 'remote', 'rename', 'origin', 'origin2') + retc, output = _get_push_output(tempdir_factory, remote='origin2') + assert retc == 0 + assert '\nremote: origin2\n' in output + + def test_pre_push_integration_empty_push(tempdir_factory, store): upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo') path = tempdir_factory.get() diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index e56f53908..87eef2ec2 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -456,8 +456,11 @@ def test_origin_source_error_msg_error( assert b'Specify both --origin and --source.' in printed -def test_origin_source_both_ok(cap_out, store, repo_with_passing_hook): - args = run_opts(origin='master', source='master') +def test_all_push_options_ok(cap_out, store, repo_with_passing_hook): + args = run_opts( + origin='master', source='master', + remote_name='origin', remote_url='https://example.com/repo', + ) ret, printed = _do_run(cap_out, store, repo_with_passing_hook, args) assert ret == 0 assert b'Specify both --origin and --source.' not in printed @@ -687,35 +690,6 @@ def _run_for_stage(stage): assert _run_for_stage('commit-msg').startswith(b'hook 5...') -def test_push_remote_environment(cap_out, store, repo_with_passing_hook): - config = { - 'repo': 'local', - 'hooks': [ - { - 'id': 'print-push-remote', - 'name': 'Print push remote name', - 'entry': 'entry: bash -c \'echo "$PRE_COMMIT_REMOTE_NAME"\'', - 'language': 'system', - 'verbose': bool(1), - }, - ], - } - add_config_to_repo(repo_with_passing_hook, config) - - _test_run( - cap_out, - store, - repo_with_passing_hook, - opts={ - 'push_remote_name': 'origin', - 'push_remote_url': 'https://github.com/pre-commit/pre-commit', - }, - expected_outputs=[b'Print push remote name', b'Passed'], - expected_ret=0, - stage=['push'], - ) - - def test_commit_msg_hook(cap_out, store, commit_msg_repo): filename = '.git/COMMIT_EDITMSG' with open(filename, 'w') as f: From 32d32e3743e6a610c4459153b92242af9d81f438 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 21 Jan 2020 14:58:03 -0800 Subject: [PATCH 540/544] work around broken bash in azure pipelines --- tests/commands/install_uninstall_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 984ae74af..24f367769 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -307,7 +307,7 @@ def test_failing_hooks_returns_nonzero(tempdir_factory, store): def _write_legacy_hook(path): os.makedirs(os.path.join(path, '.git/hooks'), exist_ok=True) with open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: - f.write('#!/usr/bin/env bash\necho "legacy hook"\n') + f.write(f'{shebang()}\nprint("legacy hook")\n') make_executable(f.name) From d9800ad95a94b8b7ef7d0a0c888b8a9b62f4dc77 Mon Sep 17 00:00:00 2001 From: Michael Schier Date: Tue, 21 Jan 2020 07:16:43 +0100 Subject: [PATCH 541/544] exclude GIT_SSL_NO_VERIFY env variable from getting stripped --- pre_commit/git.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 72a42545d..edde4b08d 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -35,7 +35,10 @@ def no_git_env(_env: Optional[EnvironT] = None) -> Dict[str, str]: return { k: v for k, v in _env.items() if not k.startswith('GIT_') or - k in {'GIT_EXEC_PATH', 'GIT_SSH', 'GIT_SSH_COMMAND', 'GIT_SSL_CAINFO'} + k in { + 'GIT_EXEC_PATH', 'GIT_SSH', 'GIT_SSH_COMMAND', 'GIT_SSL_CAINFO', + 'GIT_SSL_NO_VERIFY', + } } From 95b8d71bd98cd91a4dad63aa0f8097ed8af2adaa Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Jan 2020 13:32:33 -0800 Subject: [PATCH 542/544] Move most of the actual hook script into `pre-commit hook-impl` --- pre_commit/commands/hook_impl.py | 180 ++++++++++++++++++ pre_commit/commands/install_uninstall.py | 12 +- pre_commit/languages/python.py | 2 +- pre_commit/main.py | 21 +++ pre_commit/parse_shebang.py | 6 +- pre_commit/resources/hook-tmpl | 213 +++------------------ tests/commands/hook_impl_test.py | 225 +++++++++++++++++++++++ tests/commands/install_uninstall_test.py | 3 +- tests/main_test.py | 4 +- tests/parse_shebang_test.py | 6 +- 10 files changed, 471 insertions(+), 201 deletions(-) create mode 100644 pre_commit/commands/hook_impl.py create mode 100644 tests/commands/hook_impl_test.py diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py new file mode 100644 index 000000000..0916c02bb --- /dev/null +++ b/pre_commit/commands/hook_impl.py @@ -0,0 +1,180 @@ +import argparse +import os.path +import subprocess +import sys +from typing import Optional +from typing import Sequence +from typing import Tuple + +from pre_commit.commands.run import run +from pre_commit.envcontext import envcontext +from pre_commit.parse_shebang import normalize_cmd +from pre_commit.store import Store + +Z40 = '0' * 40 + + +def _run_legacy( + hook_type: str, + hook_dir: str, + args: Sequence[str], +) -> Tuple[int, bytes]: + if os.environ.get('PRE_COMMIT_RUNNING_LEGACY'): + raise SystemExit( + f"bug: pre-commit's script is installed in migration mode\n" + f'run `pre-commit install -f --hook-type {hook_type}` to fix ' + f'this\n\n' + f'Please report this bug at ' + f'https://github.com/pre-commit/pre-commit/issues', + ) + + if hook_type == 'pre-push': + stdin = sys.stdin.buffer.read() + else: + stdin = b'' + + # not running in legacy mode + legacy_hook = os.path.join(hook_dir, f'{hook_type}.legacy') + if not os.access(legacy_hook, os.X_OK): + return 0, stdin + + with envcontext((('PRE_COMMIT_RUNNING_LEGACY', '1'),)): + cmd = normalize_cmd((legacy_hook, *args)) + return subprocess.run(cmd, input=stdin).returncode, stdin + + +def _validate_config( + retv: int, + config: str, + skip_on_missing_config: bool, +) -> None: + if not os.path.isfile(config): + if skip_on_missing_config or os.getenv('PRE_COMMIT_ALLOW_NO_CONFIG'): + print(f'`{config}` config file not found. Skipping `pre-commit`.') + raise SystemExit(retv) + else: + print( + f'No {config} file was found\n' + f'- To temporarily silence this, run ' + f'`PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`\n' + f'- To permanently silence this, install pre-commit with the ' + f'--allow-missing-config option\n' + f'- To uninstall pre-commit run `pre-commit uninstall`', + ) + raise SystemExit(1) + + +def _ns( + hook_type: str, + color: bool, + *, + all_files: bool = False, + origin: Optional[str] = None, + source: Optional[str] = None, + remote_name: Optional[str] = None, + remote_url: Optional[str] = None, + commit_msg_filename: Optional[str] = None, +) -> argparse.Namespace: + return argparse.Namespace( + color=color, + hook_stage=hook_type.replace('pre-', ''), + origin=origin, + source=source, + remote_name=remote_name, + remote_url=remote_url, + commit_msg_filename=commit_msg_filename, + all_files=all_files, + files=(), + hook=None, + verbose=False, + show_diff_on_failure=False, + ) + + +def _rev_exists(rev: str) -> bool: + return not subprocess.call(('git', 'rev-list', '--quiet', rev)) + + +def _pre_push_ns( + color: bool, + args: Sequence[str], + stdin: bytes, +) -> Optional[argparse.Namespace]: + remote_name = args[0] + remote_url = args[1] + + for line in stdin.decode().splitlines(): + _, local_sha, _, remote_sha = line.split() + if local_sha == Z40: + continue + elif remote_sha != Z40 and _rev_exists(remote_sha): + return _ns( + 'pre-push', color, + origin=local_sha, source=remote_sha, + remote_name=remote_name, remote_url=remote_url, + ) + else: + # ancestors not found in remote + ancestors = subprocess.check_output(( + 'git', 'rev-list', local_sha, '--topo-order', '--reverse', + '--not', f'--remotes={remote_name}', + )).decode().strip() + if not ancestors: + continue + else: + first_ancestor = ancestors.splitlines()[0] + cmd = ('git', 'rev-list', '--max-parents=0', local_sha) + roots = set(subprocess.check_output(cmd).decode().splitlines()) + if first_ancestor in roots: + # pushing the whole tree including root commit + return _ns( + 'pre-push', color, + all_files=True, + remote_name=remote_name, remote_url=remote_url, + ) + else: + rev_cmd = ('git', 'rev-parse', f'{first_ancestor}^') + source = subprocess.check_output(rev_cmd).decode().strip() + return _ns( + 'pre-push', color, + origin=local_sha, source=source, + remote_name=remote_name, remote_url=remote_url, + ) + + # nothing to push + return None + + +def _run_ns( + hook_type: str, + color: bool, + args: Sequence[str], + stdin: bytes, +) -> Optional[argparse.Namespace]: + if hook_type == 'pre-push': + return _pre_push_ns(color, args, stdin) + elif hook_type in {'prepare-commit-msg', 'commit-msg'}: + return _ns(hook_type, color, commit_msg_filename=args[0]) + elif hook_type in {'pre-merge-commit', 'pre-commit'}: + return _ns(hook_type, color) + else: + raise AssertionError(f'unexpected hook type: {hook_type}') + + +def hook_impl( + store: Store, + *, + config: str, + color: bool, + hook_type: str, + hook_dir: str, + skip_on_missing_config: bool, + args: Sequence[str], +) -> int: + retv, stdin = _run_legacy(hook_type, hook_dir, args) + _validate_config(retv, config, skip_on_missing_config) + ns = _run_ns(hook_type, color, args, stdin) + if ns is None: + return retv + else: + return retv | run(config, store, ns) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index a9c46d90b..937217615 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -60,7 +60,7 @@ def shebang() -> str: f'python{sys.version_info[0]}', ] for path, exe in itertools.product(path_choices, exe_choices): - if os.path.exists(os.path.join(path, exe)): + if os.access(os.path.join(path, exe), os.X_OK): py = exe break else: @@ -92,12 +92,10 @@ def _install_hook_script( f'Use -f to use only pre-commit.', ) - params = { - 'CONFIG': config_file, - 'HOOK_TYPE': hook_type, - 'INSTALL_PYTHON': sys.executable, - 'SKIP_ON_MISSING_CONFIG': skip_on_missing_config, - } + args = ['hook-impl', f'--config={config_file}', f'--hook-type={hook_type}'] + if skip_on_missing_config: + args.append('--skip-on-missing-config') + params = {'INSTALL_PYTHON': sys.executable, 'ARGS': args} with open(hook_path, 'w') as hook_file: contents = resource_text('hook-tmpl') diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 1def27b0f..2a5cfe771 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -57,7 +57,7 @@ def _find_by_sys_executable() -> Optional[str]: def _norm(path: str) -> Optional[str]: _, exe = os.path.split(path.lower()) exe, _, _ = exe.partition('.exe') - if find_executable(exe) and exe not in {'python', 'pythonw'}: + if exe not in {'python', 'pythonw'} and find_executable(exe): return exe return None diff --git a/pre_commit/main.py b/pre_commit/main.py index e65d8ae8a..1d849c059 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -13,6 +13,7 @@ from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.clean import clean from pre_commit.commands.gc import gc +from pre_commit.commands.hook_impl import hook_impl from pre_commit.commands.init_templatedir import init_templatedir from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import install_hooks @@ -197,6 +198,16 @@ def main(argv: Optional[Sequence[str]] = None) -> int: _add_color_option(clean_parser) _add_config_option(clean_parser) + hook_impl_parser = subparsers.add_parser('hook-impl') + _add_color_option(hook_impl_parser) + _add_config_option(hook_impl_parser) + hook_impl_parser.add_argument('--hook-type') + hook_impl_parser.add_argument('--hook-dir') + hook_impl_parser.add_argument( + '--skip-on-missing-config', action='store_true', + ) + hook_impl_parser.add_argument(dest='rest', nargs=argparse.REMAINDER) + gc_parser = subparsers.add_parser('gc', help='Clean unused cached repos.') _add_color_option(gc_parser) _add_config_option(gc_parser) @@ -329,6 +340,16 @@ def main(argv: Optional[Sequence[str]] = None) -> int: return clean(store) elif args.command == 'gc': return gc(store) + elif args.command == 'hook-impl': + return hook_impl( + store, + config=args.config, + color=args.color, + hook_type=args.hook_type, + hook_dir=args.hook_dir, + skip_on_missing_config=args.skip_on_missing_config, + args=args.rest[1:], + ) elif args.command == 'install': return install( args.config, store, diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index 128a5c8da..3dc8dcaed 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -29,10 +29,8 @@ def find_executable( environ = _environ if _environ is not None else os.environ if 'PATHEXT' in environ: - possible_exe_names = tuple( - exe + ext.lower() for ext in environ['PATHEXT'].split(os.pathsep) - ) + (exe,) - + exts = environ['PATHEXT'].split(os.pathsep) + possible_exe_names = tuple(f'{exe}{ext}' for ext in exts) + (exe,) else: possible_exe_names = (exe,) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 573335a96..299144ec7 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -1,197 +1,44 @@ #!/usr/bin/env python3 -"""File generated by pre-commit: https://pre-commit.com""" -import distutils.spawn +# File generated by pre-commit: https://pre-commit.com +# ID: 138fd403232d2ddd5efb44317e38bf03 import os -import subprocess import sys -from typing import Callable -from typing import Dict -from typing import Tuple + +# we try our best, but the shebang of this script is difficult to determine: +# - macos doesn't ship with python3 +# - windows executables are almost always `python.exe` +# therefore we continue to support python2 for this small script +if sys.version_info < (3, 3): + from distutils.spawn import find_executable as which +else: + from shutil import which # work around https://github.com/Homebrew/homebrew-core/issues/30445 os.environ.pop('__PYVENV_LAUNCHER__', None) -HERE = os.path.dirname(os.path.abspath(__file__)) -Z40 = '0' * 40 -ID_HASH = '138fd403232d2ddd5efb44317e38bf03' # start templated -CONFIG = '' -HOOK_TYPE = '' INSTALL_PYTHON = '' -SKIP_ON_MISSING_CONFIG = False +ARGS = ['hook-impl'] # end templated +ARGS.extend(('--hook-dir', os.path.realpath(os.path.dirname(__file__)))) +ARGS.append('--') +ARGS.extend(sys.argv[1:]) + +DNE = '`pre-commit` not found. Did you forget to activate your virtualenv?' +if os.access(INSTALL_PYTHON, os.X_OK): + CMD = [INSTALL_PYTHON, '-mpre_commit'] +elif which('pre-commit'): + CMD = ['pre-commit'] +else: + raise SystemExit(DNE) +CMD.extend(ARGS) +if sys.platform == 'win32': # https://bugs.python.org/issue19124 + import subprocess -class EarlyExit(RuntimeError): - pass - - -class FatalError(RuntimeError): - pass - - -def _norm_exe(exe: str) -> Tuple[str, ...]: - """Necessary for shebang support on windows. - - roughly lifted from `identify.identify.parse_shebang` - """ - with open(exe, 'rb') as f: - if f.read(2) != b'#!': - return () - try: - first_line = f.readline().decode() - except UnicodeDecodeError: - return () - - cmd = first_line.split() - if cmd[0] == '/usr/bin/env': - del cmd[0] - return tuple(cmd) - - -def _run_legacy() -> Tuple[int, bytes]: - if __file__.endswith('.legacy'): - raise SystemExit( - f"bug: pre-commit's script is installed in migration mode\n" - f'run `pre-commit install -f --hook-type {HOOK_TYPE}` to fix ' - f'this\n\n' - f'Please report this bug at ' - f'https://github.com/pre-commit/pre-commit/issues', - ) - - if HOOK_TYPE == 'pre-push': - stdin = sys.stdin.buffer.read() - else: - stdin = b'' - - legacy_hook = os.path.join(HERE, f'{HOOK_TYPE}.legacy') - if os.access(legacy_hook, os.X_OK): - cmd = _norm_exe(legacy_hook) + (legacy_hook,) + tuple(sys.argv[1:]) - proc = subprocess.Popen(cmd, stdin=subprocess.PIPE if stdin else None) - proc.communicate(stdin) - return proc.returncode, stdin - else: - return 0, stdin - - -def _validate_config() -> None: - cmd = ('git', 'rev-parse', '--show-toplevel') - top_level = subprocess.check_output(cmd).decode().strip() - cfg = os.path.join(top_level, CONFIG) - if os.path.isfile(cfg): - pass - elif SKIP_ON_MISSING_CONFIG or os.getenv('PRE_COMMIT_ALLOW_NO_CONFIG'): - print(f'`{CONFIG}` config file not found. Skipping `pre-commit`.') - raise EarlyExit() - else: - raise FatalError( - f'No {CONFIG} file was found\n' - f'- To temporarily silence this, run ' - f'`PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`\n' - f'- To permanently silence this, install pre-commit with the ' - f'--allow-missing-config option\n' - f'- To uninstall pre-commit run ' - f'`pre-commit uninstall`', - ) - - -def _exe() -> Tuple[str, ...]: - with open(os.devnull, 'wb') as devnull: - for exe in (INSTALL_PYTHON, sys.executable): - try: - if not subprocess.call( - (exe, '-c', 'import pre_commit.main'), - stdout=devnull, stderr=devnull, - ): - return (exe, '-m', 'pre_commit.main', 'run') - except OSError: - pass - - if distutils.spawn.find_executable('pre-commit'): - return ('pre-commit', 'run') - - raise FatalError( - '`pre-commit` not found. Did you forget to activate your virtualenv?', - ) - - -def _rev_exists(rev: str) -> bool: - return not subprocess.call(('git', 'rev-list', '--quiet', rev)) - - -def _pre_push(stdin: bytes) -> Tuple[str, ...]: - remote_name = sys.argv[1] - remote_url = sys.argv[2] - - opts: Tuple[str, ...] = () - for line in stdin.decode().splitlines(): - _, local_sha, _, remote_sha = line.split() - if local_sha == Z40: - continue - elif remote_sha != Z40 and _rev_exists(remote_sha): - opts = ('--origin', local_sha, '--source', remote_sha) - else: - # ancestors not found in remote - ancestors = subprocess.check_output(( - 'git', 'rev-list', local_sha, '--topo-order', '--reverse', - '--not', f'--remotes={remote_name}', - )).decode().strip() - if not ancestors: - continue - else: - first_ancestor = ancestors.splitlines()[0] - cmd = ('git', 'rev-list', '--max-parents=0', local_sha) - roots = set(subprocess.check_output(cmd).decode().splitlines()) - if first_ancestor in roots: - # pushing the whole tree including root commit - opts = ('--all-files',) - else: - rev_cmd = ('git', 'rev-parse', f'{first_ancestor}^') - source = subprocess.check_output(rev_cmd).decode().strip() - opts = ('--origin', local_sha, '--source', source) - - if opts: - return ( - *opts, '--remote-name', remote_name, '--remote-url', remote_url, - ) + if sys.version_info < (3, 7): # https://bugs.python.org/issue25942 + raise SystemExit(subprocess.Popen(CMD).wait()) else: - # An attempt to push an empty changeset - raise EarlyExit() - - -def _opts(stdin: bytes) -> Tuple[str, ...]: - fns: Dict[str, Callable[[bytes], Tuple[str, ...]]] = { - 'prepare-commit-msg': lambda _: ('--commit-msg-filename', sys.argv[1]), - 'commit-msg': lambda _: ('--commit-msg-filename', sys.argv[1]), - 'pre-merge-commit': lambda _: (), - 'pre-commit': lambda _: (), - 'pre-push': _pre_push, - } - stage = HOOK_TYPE.replace('pre-', '') - return ('--config', CONFIG, '--hook-stage', stage) + fns[HOOK_TYPE](stdin) - - -if sys.version_info < (3, 7): # https://bugs.python.org/issue25942 - # this is the python 2.7 implementation - def _subprocess_call(cmd: Tuple[str, ...]) -> int: - return subprocess.Popen(cmd).wait() + raise SystemExit(subprocess.call(CMD)) else: - _subprocess_call = subprocess.call - - -def main() -> int: - retv, stdin = _run_legacy() - try: - _validate_config() - return retv | _subprocess_call(_exe() + _opts(stdin)) - except EarlyExit: - return retv - except FatalError as e: - print(e.args[0]) - return 1 - except KeyboardInterrupt: - return 1 - - -if __name__ == '__main__': - exit(main()) + os.execvp(CMD[0], CMD) diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py new file mode 100644 index 000000000..8fdbd0fa3 --- /dev/null +++ b/tests/commands/hook_impl_test.py @@ -0,0 +1,225 @@ +import subprocess +import sys +from unittest import mock + +import pytest + +import pre_commit.constants as C +from pre_commit import git +from pre_commit.commands import hook_impl +from pre_commit.envcontext import envcontext +from pre_commit.util import cmd_output +from pre_commit.util import make_executable +from testing.fixtures import git_dir +from testing.fixtures import sample_local_config +from testing.fixtures import write_config +from testing.util import cwd +from testing.util import git_commit + + +def test_validate_config_file_exists(tmpdir): + cfg = tmpdir.join(C.CONFIG_FILE).ensure() + hook_impl._validate_config(0, cfg, True) + + +def test_validate_config_missing(capsys): + with pytest.raises(SystemExit) as excinfo: + hook_impl._validate_config(123, 'DNE.yaml', False) + ret, = excinfo.value.args + assert ret == 1 + assert capsys.readouterr().out == ( + 'No DNE.yaml file was found\n' + '- To temporarily silence this, run ' + '`PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`\n' + '- To permanently silence this, install pre-commit with the ' + '--allow-missing-config option\n' + '- To uninstall pre-commit run `pre-commit uninstall`\n' + ) + + +def test_validate_config_skip_missing_config(capsys): + with pytest.raises(SystemExit) as excinfo: + hook_impl._validate_config(123, 'DNE.yaml', True) + ret, = excinfo.value.args + assert ret == 123 + expected = '`DNE.yaml` config file not found. Skipping `pre-commit`.\n' + assert capsys.readouterr().out == expected + + +def test_validate_config_skip_via_env_variable(capsys): + with pytest.raises(SystemExit) as excinfo: + with envcontext((('PRE_COMMIT_ALLOW_NO_CONFIG', '1'),)): + hook_impl._validate_config(0, 'DNE.yaml', False) + ret, = excinfo.value.args + assert ret == 0 + expected = '`DNE.yaml` config file not found. Skipping `pre-commit`.\n' + assert capsys.readouterr().out == expected + + +def test_run_legacy_does_not_exist(tmpdir): + retv, stdin = hook_impl._run_legacy('pre-commit', tmpdir, ()) + assert (retv, stdin) == (0, b'') + + +def test_run_legacy_executes_legacy_script(tmpdir, capfd): + hook = tmpdir.join('pre-commit.legacy') + hook.write('#!/usr/bin/env bash\necho hi "$@"\nexit 1\n') + make_executable(hook) + retv, stdin = hook_impl._run_legacy('pre-commit', tmpdir, ('arg1', 'arg2')) + assert capfd.readouterr().out.strip() == 'hi arg1 arg2' + assert (retv, stdin) == (1, b'') + + +def test_run_legacy_pre_push_returns_stdin(tmpdir): + with mock.patch.object(sys.stdin.buffer, 'read', return_value=b'stdin'): + retv, stdin = hook_impl._run_legacy('pre-push', tmpdir, ()) + assert (retv, stdin) == (0, b'stdin') + + +def test_run_legacy_recursive(tmpdir): + hook = tmpdir.join('pre-commit.legacy').ensure() + make_executable(hook) + + # simulate a call being recursive + def call(*_, **__): + return hook_impl._run_legacy('pre-commit', tmpdir, ()) + + with mock.patch.object(subprocess, 'run', call): + with pytest.raises(SystemExit): + call() + + +def test_run_ns_pre_commit(): + ns = hook_impl._run_ns('pre-commit', True, (), b'') + assert ns is not None + assert ns.hook_stage == 'commit' + assert ns.color is True + + +def test_run_ns_commit_msg(): + ns = hook_impl._run_ns('commit-msg', False, ('.git/COMMIT_MSG',), b'') + assert ns is not None + assert ns.hook_stage == 'commit-msg' + assert ns.color is False + assert ns.commit_msg_filename == '.git/COMMIT_MSG' + + +@pytest.fixture +def push_example(tempdir_factory): + src = git_dir(tempdir_factory) + git_commit(cwd=src) + src_head = git.head_rev(src) + + clone = tempdir_factory.get() + cmd_output('git', 'clone', src, clone) + git_commit(cwd=clone) + clone_head = git.head_rev(clone) + return (src, src_head, clone, clone_head) + + +def test_run_ns_pre_push_updating_branch(push_example): + src, src_head, clone, clone_head = push_example + + with cwd(clone): + args = ('origin', src) + stdin = f'HEAD {clone_head} refs/heads/b {src_head}\n'.encode() + ns = hook_impl._run_ns('pre-push', False, args, stdin) + + assert ns is not None + assert ns.hook_stage == 'push' + assert ns.color is False + assert ns.remote_name == 'origin' + assert ns.remote_url == src + assert ns.source == src_head + assert ns.origin == clone_head + assert ns.all_files is False + + +def test_run_ns_pre_push_new_branch(push_example): + src, src_head, clone, clone_head = push_example + + with cwd(clone): + args = ('origin', src) + stdin = f'HEAD {clone_head} refs/heads/b {hook_impl.Z40}\n'.encode() + ns = hook_impl._run_ns('pre-push', False, args, stdin) + + assert ns is not None + assert ns.source == src_head + assert ns.origin == clone_head + + +def test_run_ns_pre_push_new_branch_existing_rev(push_example): + src, src_head, clone, _ = push_example + + with cwd(clone): + args = ('origin', src) + stdin = f'HEAD {src_head} refs/heads/b2 {hook_impl.Z40}\n'.encode() + ns = hook_impl._run_ns('pre-push', False, args, stdin) + + assert ns is None + + +def test_pushing_orphan_branch(push_example): + src, src_head, clone, _ = push_example + + cmd_output('git', 'checkout', '--orphan', 'b2', cwd=clone) + git_commit(cwd=clone, msg='something else to get unique hash') + clone_rev = git.head_rev(clone) + + with cwd(clone): + args = ('origin', src) + stdin = f'HEAD {clone_rev} refs/heads/b2 {hook_impl.Z40}\n'.encode() + ns = hook_impl._run_ns('pre-push', False, args, stdin) + + assert ns is not None + assert ns.all_files is True + + +def test_run_ns_pre_push_deleting_branch(push_example): + src, src_head, clone, _ = push_example + + with cwd(clone): + args = ('origin', src) + stdin = f'(delete) {hook_impl.Z40} refs/heads/b {src_head}'.encode() + ns = hook_impl._run_ns('pre-push', False, args, stdin) + + assert ns is None + + +def test_hook_impl_main_noop_pre_push(cap_out, store, push_example): + src, src_head, clone, _ = push_example + + stdin = f'(delete) {hook_impl.Z40} refs/heads/b {src_head}'.encode() + with mock.patch.object(sys.stdin.buffer, 'read', return_value=stdin): + with cwd(clone): + write_config('.', sample_local_config()) + ret = hook_impl.hook_impl( + store, + config=C.CONFIG_FILE, + color=False, + hook_type='pre-push', + hook_dir='.git/hooks', + skip_on_missing_config=False, + args=('origin', src), + ) + assert ret == 0 + assert cap_out.get() == '' + + +def test_hook_impl_main_runs_hooks(cap_out, tempdir_factory, store): + with cwd(git_dir(tempdir_factory)): + write_config('.', sample_local_config()) + ret = hook_impl.hook_impl( + store, + config=C.CONFIG_FILE, + color=False, + hook_type='pre-commit', + hook_dir='.git/hooks', + skip_on_missing_config=False, + args=(), + ) + assert ret == 0 + expected = '''\ +Block if "DO NOT COMMIT" is found....................(no files to check)Skipped +''' + assert cap_out.get() == expected diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 24f367769..6d4861490 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -51,7 +51,8 @@ def test_shebang_posix_not_on_path(): def test_shebang_posix_on_path(tmpdir): - tmpdir.join(f'python{sys.version_info[0]}').ensure() + exe = tmpdir.join(f'python{sys.version_info[0]}').ensure() + make_executable(exe) with mock.patch.object(sys, 'platform', 'posix'): with mock.patch.object(os, 'defpath', tmpdir.strpath): diff --git a/tests/main_test.py b/tests/main_test.py index 6a084dca9..c4724768c 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -81,8 +81,8 @@ def test_adjust_args_try_repo_repo_relative(in_git_dir): FNS = ( - 'autoupdate', 'clean', 'gc', 'install', 'install_hooks', 'migrate_config', - 'run', 'sample_config', 'uninstall', + 'autoupdate', 'clean', 'gc', 'hook_impl', 'install', 'install_hooks', + 'migrate_config', 'run', 'sample_config', 'uninstall', ) CMDS = tuple(fn.replace('_', '-') for fn in FNS) diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index 158e57196..62eb81e5e 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -1,6 +1,6 @@ import contextlib -import distutils.spawn -import os +import os.path +import shutil import sys import pytest @@ -12,7 +12,7 @@ def _echo_exe() -> str: - exe = distutils.spawn.find_executable('echo') + exe = shutil.which('echo') assert exe is not None return exe From d56fdca618197c68937387292de0dcc19224068d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 28 Jan 2020 12:43:18 -0800 Subject: [PATCH 543/544] allow init-templatedir to succeed when core.hooksPath is set --- pre_commit/commands/install_uninstall.py | 2 +- tests/commands/init_templatedir_test.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 937217615..b2ccc5cf1 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -123,7 +123,7 @@ def install( skip_on_missing_config: bool = False, git_dir: Optional[str] = None, ) -> int: - if git.has_core_hookpaths_set(): + if git_dir is None and git.has_core_hookpaths_set(): logger.error( 'Cowardly refusing to install hooks with `core.hooksPath` set.\n' 'hint: `git config --unset-all core.hooksPath`', diff --git a/tests/commands/init_templatedir_test.py b/tests/commands/init_templatedir_test.py index 4e32e750a..d14a171f6 100644 --- a/tests/commands/init_templatedir_test.py +++ b/tests/commands/init_templatedir_test.py @@ -79,3 +79,14 @@ def test_init_templatedir_expanduser(tmpdir, tempdir_factory, store, cap_out): lines = cap_out.get().splitlines() assert len(lines) == 1 assert lines[0].startswith('pre-commit installed at') + + +def test_init_templatedir_hookspath_set(tmpdir, tempdir_factory, store): + target = tmpdir.join('tmpl') + tmp_git_dir = git_dir(tempdir_factory) + with cwd(tmp_git_dir): + cmd_output('git', 'config', '--local', 'core.hooksPath', 'hooks') + init_templatedir( + C.CONFIG_FILE, store, target, hook_types=['pre-commit'], + ) + assert target.join('hooks/pre-commit').exists() From 0cc199d351ab126abd874a4220b4f6c11362ee71 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 28 Jan 2020 18:38:55 -0800 Subject: [PATCH 544/544] v2.0.0 --- CHANGELOG.md | 41 +++++++++++++++++++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18322ad01..8a670afab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,44 @@ +2.0.0 - 2020-01-28 +================== + +### Features +- Expose `PRE_COMMIT_REMOTE_NAME` and `PRE_COMMIT_REMOTE_URL` as environment + variables during `pre-push` hooks. + - #1274 issue by @dmbarreiro. + - #1288 PR by @dmbarreiro. + +### Fixes +- Fix `python -m pre_commit --version` to mention `pre-commit` instead of + `__main__.py`. + - #1273 issue by @ssbarnea. + - #1276 PR by @orcutt989. +- Don't filter `GIT_SSL_NO_VERIFY` from environment when cloning. + - #1293 PR by @schiermike. +- Allow `pre-commit init-templatedir` to succeed even if `core.hooksPath` is + set. + - #1298 issue by @damienrj. + - #1299 PR by @asottile. + +### Misc +- Fix changelog date for 1.21.0. + - #1275 PR by @flaudisio. + +### Updating +- Removed `pcre` language, use `pygrep` instead. + - #1268 PR by @asottile. +- Removed `--tags-only` argument to `pre-commit autoupdate` (it has done + nothing since 0.14.0). + - #1269 by @asottile. +- Remove python2 / python3.5 support. Note that pre-commit still supports + running hooks written in python2, but pre-commit itself requires python 3.6+. + - #1260 issue by @asottile. + - #1277 PR by @asottile. + - #1281 PR by @asottile. + - #1282 PR by @asottile. + - #1287 PR by @asottile. + - #1289 PR by @asottile. + - #1292 PR by @asottile. + 1.21.0 - 2020-01-02 =================== diff --git a/setup.cfg b/setup.cfg index 7dd068650..4eef854df 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.21.0 +version = 2.0.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown