From eda684661b2edc13a3698079241146a4932184ba Mon Sep 17 00:00:00 2001 From: Daniele Esposti Date: Tue, 3 Jul 2018 00:26:25 +0100 Subject: [PATCH 001/967] 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 e6b6abeb9f5e022d44d2e8d3a98755d4d2bbeb1b Mon Sep 17 00:00:00 2001 From: Daniele Esposti Date: Tue, 3 Jul 2018 01:19:58 +0100 Subject: [PATCH 002/967] 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 003/967] 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 004/967] 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 005/967] 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 006/967] 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 007/967] 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 008/967] 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 009/967] 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 010/967] 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 011/967] 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 012/967] 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 013/967] 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 014/967] 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 015/967] 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 016/967] 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 017/967] 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 018/967] 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 019/967] 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 020/967] 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 021/967] 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 022/967] 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 023/967] 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 024/967] 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 025/967] 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 026/967] 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 027/967] 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 028/967] 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 029/967] 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 030/967] 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 031/967] 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 032/967] 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 033/967] 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 034/967] 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 035/967] 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 036/967] 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 037/967] 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 038/967] 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 039/967] 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 040/967] 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 041/967] 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 042/967] 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 043/967] 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 044/967] 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 045/967] 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 046/967] 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 047/967] 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 048/967] 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 049/967] 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 050/967] 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 051/967] 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 052/967] 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 053/967] 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 054/967] 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 055/967] 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 056/967] 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 057/967] 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 058/967] 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 059/967] 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 060/967] 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 061/967] 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 062/967] 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 063/967] 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 064/967] 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 065/967] 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 066/967] 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 067/967] 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 068/967] 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 069/967] 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 070/967] 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 071/967] 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 072/967] 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 073/967] 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 074/967] 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 075/967] 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 076/967] 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 077/967] 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 078/967] 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 079/967] 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 080/967] 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 081/967] 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 082/967] 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 083/967] 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 084/967] 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 085/967] 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 086/967] 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 087/967] 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 088/967] 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 089/967] 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 090/967] 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 091/967] 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 092/967] 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 093/967] 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 094/967] 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 095/967] 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 096/967] 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 097/967] 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 098/967] 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 099/967] 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 100/967] 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 101/967] 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 102/967] 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 103/967] 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 104/967] 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 105/967] 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 106/967] 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 107/967] 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 108/967] 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 109/967] 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 110/967] 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 111/967] 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 112/967] 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 113/967] 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 114/967] 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 115/967] 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 116/967] 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 117/967] 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 118/967] 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 119/967] 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 120/967] 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 121/967] 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 122/967] 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 123/967] 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 124/967] 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 125/967] 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 126/967] 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 127/967] 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 128/967] 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 129/967] 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 130/967] 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 131/967] 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 132/967] 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 133/967] 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 134/967] 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 135/967] 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 136/967] 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 137/967] 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 138/967] 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 139/967] 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 140/967] 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 141/967] 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 142/967] 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 143/967] 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 144/967] 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 145/967] 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 146/967] 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 147/967] 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 148/967] 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 149/967] 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 150/967] 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 151/967] 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 152/967] 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 153/967] 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 154/967] 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 155/967] 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 156/967] 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 157/967] 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 158/967] 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 159/967] 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 160/967] 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 161/967] 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 162/967] 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 163/967] 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 164/967] 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 165/967] 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 166/967] 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 167/967] 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 168/967] 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 169/967] 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 170/967] 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 171/967] 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 172/967] 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 173/967] 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 174/967] 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 175/967] 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 176/967] 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 177/967] 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 178/967] 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 179/967] 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 180/967] 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 181/967] 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 182/967] 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 183/967] 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 184/967] 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 185/967] 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 186/967] 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 187/967] 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 188/967] 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 189/967] 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 190/967] 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 191/967] 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 192/967] 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 193/967] 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 194/967] 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 195/967] 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 196/967] 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 197/967] 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 198/967] 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 199/967] 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 200/967] 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 201/967] 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 202/967] 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 203/967] 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 204/967] 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 205/967] 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 206/967] 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 207/967] 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 208/967] 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 209/967] 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 210/967] 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 211/967] 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 212/967] 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 213/967] 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 214/967] 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 215/967] 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 216/967] 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 217/967] 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 218/967] 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 219/967] 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 220/967] 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 221/967] 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 222/967] 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 223/967] 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 224/967] 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 225/967] 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 226/967] 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 227/967] 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 228/967] 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 229/967] 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 230/967] 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 231/967] 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 232/967] 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 233/967] 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 234/967] 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 235/967] 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 236/967] 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 237/967] 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 238/967] 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 239/967] 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 240/967] 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 241/967] 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 242/967] 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 243/967] 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 244/967] 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 245/967] 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 246/967] 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 247/967] 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 248/967] 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 249/967] 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 250/967] 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 251/967] 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 252/967] 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 253/967] 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 254/967] 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 255/967] 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 256/967] 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 257/967] 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 258/967] 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 259/967] 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 260/967] 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 261/967] 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 262/967] 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 263/967] 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 264/967] 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 265/967] 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 266/967] 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 267/967] 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 268/967] 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 269/967] 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 270/967] 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 271/967] 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 272/967] 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 273/967] 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 274/967] [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 275/967] 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 276/967] 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 277/967] 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 278/967] 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 279/967] 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 280/967] 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 281/967] 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 282/967] 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 283/967] 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 284/967] 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 285/967] 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 286/967] 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 287/967] 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 288/967] 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 289/967] 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 290/967] 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 291/967] 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 292/967] 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 293/967] 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 294/967] 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 295/967] 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 296/967] 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 297/967] 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 298/967] 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 299/967] 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 300/967] 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 301/967] 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 302/967] 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 303/967] 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 304/967] 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 305/967] 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 306/967] 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 307/967] 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 308/967] 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 309/967] 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 310/967] 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 311/967] 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 312/967] 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 313/967] 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 314/967] 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 315/967] 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 316/967] 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 317/967] 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 318/967] 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 319/967] 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 320/967] 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 321/967] 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 322/967] 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 323/967] 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 324/967] 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 325/967] 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 326/967] 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 327/967] 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 328/967] 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 329/967] 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 From 9e4dc7f3492ccb4db1dd9e7e4d4584aafde41092 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 29 Jan 2020 17:40:16 -0800 Subject: [PATCH 330/967] Fix pre-commit in python 3.6.0-3.6.1 --- .pre-commit-config.yaml | 1 + pre_commit/languages/helpers.py | 7 +++++-- pre_commit/parse_shebang.py | 7 +++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e7c441f5c..23c19961c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,6 +15,7 @@ repos: rev: 3.7.9 hooks: - id: flake8 + additional_dependencies: [flake8-typing-imports==1.5.0] - repo: https://github.com/pre-commit/mirrors-autopep8 rev: v1.4.4 hooks: diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 3b5382912..ba96568cc 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -3,11 +3,11 @@ import random from typing import Any from typing import List -from typing import NoReturn from typing import Optional from typing import overload from typing import Sequence from typing import Tuple +from typing import TYPE_CHECKING import pre_commit.constants as C from pre_commit.hook import Hook @@ -15,6 +15,9 @@ from pre_commit.util import cmd_output_b from pre_commit.xargs import xargs +if TYPE_CHECKING: + from typing import NoReturn + FIXED_RANDOM_SEED = 1542676186 @@ -65,7 +68,7 @@ def no_install( prefix: Prefix, version: str, additional_dependencies: Sequence[str], -) -> NoReturn: +) -> 'NoReturn': raise AssertionError('This type is not installable') diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index 3dc8dcaed..7b9a05828 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -1,11 +1,14 @@ import os.path from typing import Mapping -from typing import NoReturn from typing import Optional from typing import Tuple +from typing import TYPE_CHECKING from identify.identify import parse_shebang_from_file +if TYPE_CHECKING: + from typing import NoReturn + class ExecutableNotFoundError(OSError): def to_output(self) -> Tuple[int, bytes, None]: @@ -44,7 +47,7 @@ def find_executable( def normexe(orig: str) -> str: - def _error(msg: str) -> NoReturn: + def _error(msg: str) -> 'NoReturn': raise ExecutableNotFoundError(f'Executable `{orig}` {msg}') if os.sep not in orig and (not os.altsep or os.altsep not in orig): From f0ee93c5a7ee84a895918a0c0d1bc269233ccb7f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 29 Jan 2020 17:57:05 -0800 Subject: [PATCH 331/967] v2.0.1 --- CHANGELOG.md | 7 +++++++ setup.cfg | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a670afab..2ef3739b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +2.0.1 - 2020-01-29 +================== + +### Fixes +- Fix `ImportError` in python 3.6.0 / 3.6.1 for `typing.NoReturn` + - #1302 PR by @asottile. + 2.0.0 - 2020-01-28 ================== diff --git a/setup.cfg b/setup.cfg index 4eef854df..4e42ddc49 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.0.0 +version = 2.0.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From bb29630d57440f3ee6bb7e787942f323d502b126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 28 Jan 2020 23:25:24 +0200 Subject: [PATCH 332/967] First cut at Perl hook support --- pre_commit/languages/all.py | 2 ++ pre_commit/languages/perl.py | 66 ++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 pre_commit/languages/perl.py diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index e6d7b1dbc..8f4ffa8c5 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -11,6 +11,7 @@ from pre_commit.languages import fail from pre_commit.languages import golang from pre_commit.languages import node +from pre_commit.languages import perl from pre_commit.languages import pygrep from pre_commit.languages import python from pre_commit.languages import python_venv @@ -45,6 +46,7 @@ class Language(NamedTuple): 'fail': Language(name='fail', ENVIRONMENT_DIR=fail.ENVIRONMENT_DIR, get_default_version=fail.get_default_version, healthy=fail.healthy, install_environment=fail.install_environment, run_hook=fail.run_hook), # noqa: E501 'golang': Language(name='golang', ENVIRONMENT_DIR=golang.ENVIRONMENT_DIR, get_default_version=golang.get_default_version, healthy=golang.healthy, install_environment=golang.install_environment, run_hook=golang.run_hook), # noqa: E501 'node': Language(name='node', ENVIRONMENT_DIR=node.ENVIRONMENT_DIR, get_default_version=node.get_default_version, healthy=node.healthy, install_environment=node.install_environment, run_hook=node.run_hook), # noqa: E501 + 'perl': Language(name='perl', ENVIRONMENT_DIR=perl.ENVIRONMENT_DIR, get_default_version=perl.get_default_version, healthy=perl.healthy, install_environment=perl.install_environment, run_hook=perl.run_hook), # noqa: E501 'pygrep': Language(name='pygrep', ENVIRONMENT_DIR=pygrep.ENVIRONMENT_DIR, get_default_version=pygrep.get_default_version, healthy=pygrep.healthy, install_environment=pygrep.install_environment, run_hook=pygrep.run_hook), # noqa: E501 'python': Language(name='python', ENVIRONMENT_DIR=python.ENVIRONMENT_DIR, get_default_version=python.get_default_version, healthy=python.healthy, install_environment=python.install_environment, run_hook=python.run_hook), # noqa: E501 'python_venv': Language(name='python_venv', ENVIRONMENT_DIR=python_venv.ENVIRONMENT_DIR, get_default_version=python_venv.get_default_version, healthy=python_venv.healthy, install_environment=python_venv.install_environment, run_hook=python_venv.run_hook), # noqa: E501 diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py new file mode 100644 index 000000000..52d8aab9e --- /dev/null +++ b/pre_commit/languages/perl.py @@ -0,0 +1,66 @@ +import contextlib +import os +from typing import Generator +from typing import Sequence +from typing import Tuple + +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import Var +from pre_commit.hook import Hook +from pre_commit.languages import helpers +from pre_commit.prefix import Prefix +from pre_commit.util import clean_path_on_failure + +ENVIRONMENT_DIR = 'perl_env' +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy + + +def _envdir(prefix: Prefix, version: str) -> str: + directory = helpers.environment_dir(ENVIRONMENT_DIR, version) + return prefix.path(directory) + + +def get_env_patch(venv: str) -> PatchesT: + return ( + ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), + ('PERL5LIB', os.path.join(venv, 'lib', 'perl5')), + ('PERL_MB_OPT', f'--install_base {venv}'), + ( + 'PERL_MM_OPT', ( + f'INSTALL_BASE={venv}' + ' INSTALLSITEMAN1DIR=none INSTALLSITEMAN3DIR=none' + ), + ), + ) + + +@contextlib.contextmanager +def in_env( + prefix: Prefix, + language_version: str, +) -> Generator[None, None, None]: + with envcontext(get_env_patch(_envdir(prefix, language_version))): + yield + + +def install_environment( + prefix: Prefix, version: str, additional_dependencies: Sequence[str], +) -> None: + helpers.assert_version_default('perl', version) + + with clean_path_on_failure(_envdir(prefix, version)): + with in_env(prefix, version): + helpers.run_setup_cmd( + prefix, ('cpan', '-T', '.', *additional_dependencies), + ) + + +def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: + with in_env(hook.prefix, hook.language_version): + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) From a64fa6d478da6a5b9a2f8fcfd69ea77413ff8569 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 31 Jan 2020 17:18:59 -0800 Subject: [PATCH 333/967] Replace aspy.yaml with sort_keys=False --- pre_commit/clientlib.py | 6 +++--- pre_commit/commands/autoupdate.py | 9 ++++----- pre_commit/commands/migrate_config.py | 7 ++++--- pre_commit/commands/try_repo.py | 5 ++--- pre_commit/constants.py | 2 -- pre_commit/util.py | 14 ++++++++++++++ setup.cfg | 3 +-- testing/fixtures.py | 16 ++++++++-------- 8 files changed, 36 insertions(+), 26 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 43e2c8ec5..56ec0dd1b 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -9,13 +9,13 @@ from typing import Sequence import cfgv -from aspy.yaml import ordered_load from identify.identify import ALL_TAGS import pre_commit.constants as C from pre_commit.error_handler import FatalError from pre_commit.languages.all import all_languages from pre_commit.util import parse_version +from pre_commit.util import yaml_load logger = logging.getLogger('pre_commit') @@ -84,7 +84,7 @@ class InvalidManifestError(FatalError): load_manifest = functools.partial( cfgv.load_from_filename, schema=MANIFEST_SCHEMA, - load_strategy=ordered_load, + load_strategy=yaml_load, exc_tp=InvalidManifestError, ) @@ -288,7 +288,7 @@ class InvalidConfigError(FatalError): def ordered_load_normalize_legacy_config(contents: str) -> Dict[str, Any]: - data = ordered_load(contents) + data = yaml_load(contents) if isinstance(data, list): # TODO: Once happy, issue a deprecation warning and instructions return {'repos': data} diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index fd98118ab..5a9a9880c 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -8,9 +8,6 @@ from typing import Sequence from typing import Tuple -from aspy.yaml import ordered_dump -from aspy.yaml import ordered_load - import pre_commit.constants as C from pre_commit import git from pre_commit import output @@ -25,6 +22,8 @@ from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b from pre_commit.util import tmpdir +from pre_commit.util import yaml_dump +from pre_commit.util import yaml_load class RevInfo(NamedTuple): @@ -105,7 +104,7 @@ def _original_lines( raise AssertionError('could not find rev lines') else: with open(path, 'w') as f: - f.write(ordered_dump(ordered_load(original), **C.YAML_DUMP_KWARGS)) + f.write(yaml_dump(yaml_load(original))) return _original_lines(path, rev_infos, retry=True) @@ -117,7 +116,7 @@ def _write_new_config(path: str, rev_infos: List[Optional[RevInfo]]) -> None: continue match = REV_LINE_RE.match(lines[idx]) assert match is not None - new_rev_s = ordered_dump({'rev': rev_info.rev}, **C.YAML_DUMP_KWARGS) + new_rev_s = yaml_dump({'rev': rev_info.rev}) new_rev = new_rev_s.split(':', 1)[1].strip() if rev_info.frozen is not None: comment = f' # frozen: {rev_info.frozen}' diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index 5b90b6f6b..d83b8e9cf 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -1,7 +1,8 @@ import re import yaml -from aspy.yaml import ordered_load + +from pre_commit.util import yaml_load def _indent(s: str) -> str: @@ -24,12 +25,12 @@ def _migrate_map(contents: str) -> str: header = ''.join(lines[:i]) rest = ''.join(lines[i:]) - if isinstance(ordered_load(contents), list): + if isinstance(yaml_load(contents), list): # If they are using the "default" flow style of yaml, this operation # will yield a valid configuration try: trial_contents = f'{header}repos:\n{rest}' - ordered_load(trial_contents) + yaml_load(trial_contents) contents = trial_contents except yaml.YAMLError: contents = f'{header}repos:\n{_indent(rest)}' diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index 989a0c12c..4aee209c6 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -4,8 +4,6 @@ from typing import Optional from typing import Tuple -from aspy.yaml import ordered_dump - import pre_commit.constants as C from pre_commit import git from pre_commit import output @@ -14,6 +12,7 @@ from pre_commit.store import Store from pre_commit.util import cmd_output_b from pre_commit.util import tmpdir +from pre_commit.util import yaml_dump from pre_commit.xargs import xargs logger = logging.getLogger(__name__) @@ -63,7 +62,7 @@ def try_repo(args: argparse.Namespace) -> int: hooks = [{'id': hook['id']} for hook in manifest] config = {'repos': [{'repo': repo, 'rev': ref, 'hooks': hooks}]} - config_s = ordered_dump(config, **C.YAML_DUMP_KWARGS) + config_s = yaml_dump(config) config_filename = os.path.join(tempdir, C.CONFIG_FILE) with open(config_filename, 'w') as cfg: diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 0fc740b28..23622ecbf 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -8,8 +8,6 @@ CONFIG_FILE = '.pre-commit-config.yaml' MANIFEST_FILE = '.pre-commit-hooks.yaml' -YAML_DUMP_KWARGS = {'default_flow_style': False, 'indent': 4} - # Bump when installation changes in a backwards / forwards incompatible way INSTALLED_STATE_VERSION = '1' # Bump when modifying `empty_template` diff --git a/pre_commit/util.py b/pre_commit/util.py index dfe07ea9c..65775710d 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -1,5 +1,6 @@ import contextlib import errno +import functools import os.path import shutil import stat @@ -17,6 +18,8 @@ from typing import Type from typing import Union +import yaml + from pre_commit import parse_shebang if sys.version_info >= (3, 7): # pragma: no cover (PY37+) @@ -28,6 +31,17 @@ EnvironT = Union[Dict[str, str], 'os._Environ'] +Loader = getattr(yaml, 'CSafeLoader', yaml.SafeLoader) +yaml_load = functools.partial(yaml.load, Loader=Loader) +Dumper = getattr(yaml, 'CSafeDumper', yaml.SafeDumper) + + +def yaml_dump(o: Any) -> str: + # when python/mypy#1484 is solved, this can be `functools.partial` + return yaml.dump( + o, Dumper=Dumper, default_flow_style=False, indent=4, sort_keys=False, + ) + @contextlib.contextmanager def clean_path_on_failure(path: str) -> Generator[None, None, None]: diff --git a/setup.cfg b/setup.cfg index 4e42ddc49..bf5c01c3d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,11 +22,10 @@ classifiers = [options] packages = find: install_requires = - aspy.yaml cfgv>=2.0.0 identify>=1.0.0 nodeenv>=0.11.1 - pyyaml + pyyaml>=5.1 toml virtualenv>=15.2 importlib-metadata;python_version<"3.8" diff --git a/testing/fixtures.py b/testing/fixtures.py index a9f54a22a..f7def081f 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -2,8 +2,6 @@ import os.path import shutil -from aspy.yaml import ordered_dump -from aspy.yaml import ordered_load from cfgv import apply_defaults from cfgv import validate @@ -12,6 +10,8 @@ from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest from pre_commit.util import cmd_output +from pre_commit.util import yaml_dump +from pre_commit.util import yaml_load from testing.util import get_resource_path from testing.util import git_commit @@ -55,10 +55,10 @@ def modify_manifest(path, commit=True): """ manifest_path = os.path.join(path, C.MANIFEST_FILE) with open(manifest_path) as f: - manifest = ordered_load(f.read()) + manifest = yaml_load(f.read()) yield manifest with open(manifest_path, 'w') as manifest_file: - manifest_file.write(ordered_dump(manifest, **C.YAML_DUMP_KWARGS)) + manifest_file.write(yaml_dump(manifest)) if commit: git_commit(msg=modify_manifest.__name__, cwd=path) @@ -70,10 +70,10 @@ def modify_config(path='.', commit=True): """ config_path = os.path.join(path, C.CONFIG_FILE) with open(config_path) as f: - config = ordered_load(f.read()) + config = yaml_load(f.read()) yield config with open(config_path, 'w', encoding='UTF-8') as config_file: - config_file.write(ordered_dump(config, **C.YAML_DUMP_KWARGS)) + config_file.write(yaml_dump(config)) if commit: git_commit(msg=modify_config.__name__, cwd=path) @@ -114,7 +114,7 @@ def make_config_from_repo(repo_path, rev=None, hooks=None, check=True): def read_config(directory, config_file=C.CONFIG_FILE): config_path = os.path.join(directory, config_file) with open(config_path) as f: - config = ordered_load(f.read()) + config = yaml_load(f.read()) return config @@ -123,7 +123,7 @@ def write_config(directory, config, config_file=C.CONFIG_FILE): assert isinstance(config, dict), config config = {'repos': [config]} with open(os.path.join(directory, config_file), 'w') as outfile: - outfile.write(ordered_dump(config, **C.YAML_DUMP_KWARGS)) + outfile.write(yaml_dump(config)) def add_config_to_repo(git_path, config, config_file=C.CONFIG_FILE): From aee7843bec755150a897faf0d62ca981a75f88ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 1 Feb 2020 15:20:25 +0200 Subject: [PATCH 334/967] Add perl to gen-languages-all --- testing/gen-languages-all | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/testing/gen-languages-all b/testing/gen-languages-all index add6752dc..6d0b26ff9 100755 --- a/testing/gen-languages-all +++ b/testing/gen-languages-all @@ -2,8 +2,9 @@ import sys LANGUAGES = [ - 'conda', 'docker', 'docker_image', 'fail', 'golang', 'node', 'pygrep', - 'python', 'python_venv', 'ruby', 'rust', 'script', 'swift', 'system', + 'conda', 'docker', 'docker_image', 'fail', 'golang', 'node', 'perl', + 'pygrep', 'python', 'python_venv', 'ruby', 'rust', 'script', 'swift', + 'system', ] FIELDS = [ 'ENVIRONMENT_DIR', 'get_default_version', 'healthy', 'install_environment', From 129536498619cc4241ed6424335c9ff93a7222e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 1 Feb 2020 15:41:14 +0200 Subject: [PATCH 335/967] Add basic perl repo test --- testing/resources/perl_hooks_repo/.gitignore | 7 +++++++ .../resources/perl_hooks_repo/.pre-commit-hooks.yaml | 5 +++++ testing/resources/perl_hooks_repo/MANIFEST | 4 ++++ testing/resources/perl_hooks_repo/Makefile.PL | 10 ++++++++++ .../perl_hooks_repo/bin/pre-commit-perl-hello | 7 +++++++ .../resources/perl_hooks_repo/lib/PreCommitHello.pm | 12 ++++++++++++ tests/repository_test.py | 7 +++++++ 7 files changed, 52 insertions(+) create mode 100644 testing/resources/perl_hooks_repo/.gitignore create mode 100644 testing/resources/perl_hooks_repo/.pre-commit-hooks.yaml create mode 100644 testing/resources/perl_hooks_repo/MANIFEST create mode 100644 testing/resources/perl_hooks_repo/Makefile.PL create mode 100755 testing/resources/perl_hooks_repo/bin/pre-commit-perl-hello create mode 100644 testing/resources/perl_hooks_repo/lib/PreCommitHello.pm diff --git a/testing/resources/perl_hooks_repo/.gitignore b/testing/resources/perl_hooks_repo/.gitignore new file mode 100644 index 000000000..7af994045 --- /dev/null +++ b/testing/resources/perl_hooks_repo/.gitignore @@ -0,0 +1,7 @@ +/MYMETA.json +/MYMETA.yml +/Makefile +/PreCommitHello-*.tar.* +/PreCommitHello-*/ +/blib/ +/pm_to_blib diff --git a/testing/resources/perl_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/perl_hooks_repo/.pre-commit-hooks.yaml new file mode 100644 index 000000000..11e6f6cd9 --- /dev/null +++ b/testing/resources/perl_hooks_repo/.pre-commit-hooks.yaml @@ -0,0 +1,5 @@ +- id: perl-hook + name: perl example hook + entry: pre-commit-perl-hello + language: perl + files: '' diff --git a/testing/resources/perl_hooks_repo/MANIFEST b/testing/resources/perl_hooks_repo/MANIFEST new file mode 100644 index 000000000..4a20084c6 --- /dev/null +++ b/testing/resources/perl_hooks_repo/MANIFEST @@ -0,0 +1,4 @@ +MANIFEST +Makefile.PL +bin/pre-commit-perl-hello +lib/PreCommitHello.pm diff --git a/testing/resources/perl_hooks_repo/Makefile.PL b/testing/resources/perl_hooks_repo/Makefile.PL new file mode 100644 index 000000000..6c70e1071 --- /dev/null +++ b/testing/resources/perl_hooks_repo/Makefile.PL @@ -0,0 +1,10 @@ +use strict; +use warnings; + +use ExtUtils::MakeMaker; + +WriteMakefile( + NAME => "PreCommitHello", + VERSION_FROM => "lib/PreCommitHello.pm", + EXE_FILES => [qw(bin/pre-commit-perl-hello)], +); diff --git a/testing/resources/perl_hooks_repo/bin/pre-commit-perl-hello b/testing/resources/perl_hooks_repo/bin/pre-commit-perl-hello new file mode 100755 index 000000000..9474009a1 --- /dev/null +++ b/testing/resources/perl_hooks_repo/bin/pre-commit-perl-hello @@ -0,0 +1,7 @@ +#!/usr/bin/env perl + +use strict; +use warnings; +use PreCommitHello; + +PreCommitHello::hello(); diff --git a/testing/resources/perl_hooks_repo/lib/PreCommitHello.pm b/testing/resources/perl_hooks_repo/lib/PreCommitHello.pm new file mode 100644 index 000000000..c76521cea --- /dev/null +++ b/testing/resources/perl_hooks_repo/lib/PreCommitHello.pm @@ -0,0 +1,12 @@ +package PreCommitHello; + +use strict; +use warnings; + +our $VERSION = "0.1.0"; + +sub hello { + print "Hello from perl-commit Perl!\n"; +} + +1; diff --git a/tests/repository_test.py b/tests/repository_test.py index 21f2f41ce..6fcf5e5d4 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -876,3 +876,10 @@ def test_manifest_hooks(tempdir_factory, store): types=['file'], verbose=False, ) + + +def test_perl_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'perl_hooks_repo', + 'perl-hook', [], b'Hello from perl-commit Perl!\n', + ) From 04471f7d9795f6d7aee49a9db710bf0c27a21866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 1 Feb 2020 16:13:01 +0200 Subject: [PATCH 336/967] Add perl additional dependencies test --- pre_commit/resources/empty_template_Makefile.PL | 6 ++++++ pre_commit/store.py | 1 + tests/repository_test.py | 17 +++++++++++++++++ 3 files changed, 24 insertions(+) create mode 100644 pre_commit/resources/empty_template_Makefile.PL diff --git a/pre_commit/resources/empty_template_Makefile.PL b/pre_commit/resources/empty_template_Makefile.PL new file mode 100644 index 000000000..ac75fe531 --- /dev/null +++ b/pre_commit/resources/empty_template_Makefile.PL @@ -0,0 +1,6 @@ +use ExtUtils::MakeMaker; + +WriteMakefile( + NAME => "PreCommitDummy", + VERSION => "0.0.1", +); diff --git a/pre_commit/store.py b/pre_commit/store.py index 4af161937..760b37aaf 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -184,6 +184,7 @@ def _git_cmd(*args: str) -> None: LOCAL_RESOURCES = ( 'Cargo.toml', 'main.go', 'main.rs', '.npmignore', 'package.json', 'pre_commit_dummy_package.gemspec', 'setup.py', 'environment.yml', + 'Makefile.PL', ) def make_local(self, deps: Sequence[str]) -> str: diff --git a/tests/repository_test.py b/tests/repository_test.py index 6fcf5e5d4..b745a9aa3 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -883,3 +883,20 @@ def test_perl_hook(tempdir_factory, store): tempdir_factory, store, 'perl_hooks_repo', 'perl-hook', [], b'Hello from perl-commit Perl!\n', ) + + +def test_local_perl_additional_dependencies(store): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'hello', + 'name': 'hello', + 'entry': 'perltidy --version', + 'language': 'perl', + 'additional_dependencies': ['SHANCOCK/Perl-Tidy-20200110.tar.gz'], + }], + } + hook = _get_hook(config, store, 'hello') + ret, out = _hook_run(hook, (), color=False) + assert ret == 0 + assert _norm_out(out).startswith(b'This is perltidy, v20200110') From 44f5753bd83080b39a42566278d76e5b51918846 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 3 Feb 2020 10:39:08 -0800 Subject: [PATCH 337/967] shlex-quote install path to fix windows --- pre_commit/languages/perl.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py index 52d8aab9e..f61815aa9 100644 --- a/pre_commit/languages/perl.py +++ b/pre_commit/languages/perl.py @@ -1,5 +1,6 @@ import contextlib import os +import shlex from typing import Generator from typing import Sequence from typing import Tuple @@ -26,11 +27,11 @@ def get_env_patch(venv: str) -> PatchesT: return ( ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), ('PERL5LIB', os.path.join(venv, 'lib', 'perl5')), - ('PERL_MB_OPT', f'--install_base {venv}'), + ('PERL_MB_OPT', f'--install_base {shlex.quote(venv)}'), ( 'PERL_MM_OPT', ( - f'INSTALL_BASE={venv}' - ' INSTALLSITEMAN1DIR=none INSTALLSITEMAN3DIR=none' + f'INSTALL_BASE={shlex.quote(venv)} ' + f'INSTALLSITEMAN1DIR=none INSTALLSITEMAN3DIR=none' ), ), ) From 977bbd7643e9df9769a76e6e7b9502cfed05b91c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 3 Feb 2020 12:42:10 -0800 Subject: [PATCH 338/967] put strawberry perl on the beginning of the PATH for windows --- azure-pipelines.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b9f0b5f3b..c51b4a5f7 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -24,6 +24,11 @@ jobs: pre_test: - powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts" displayName: Add conda to PATH + - powershell: | + Write-Host "##vso[task.prependpath]C:\Strawberry\perl\bin" + Write-Host "##vso[task.prependpath]C:\Strawberry\perl\site\bin" + Write-Host "##vso[task.prependpath]C:\Strawberry\c\bin" + displayName: Add strawberry perl to PATH - template: job--python-tox.yml@asottile parameters: toxenvs: [py37] From 8d2af32e4d02de4a2e3c70bccd337fd738a47a56 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 3 Feb 2020 14:06:51 -0800 Subject: [PATCH 339/967] delete unused testing/latest-git.sh --- testing/latest-git.sh | 7 ------- 1 file changed, 7 deletions(-) delete mode 100755 testing/latest-git.sh diff --git a/testing/latest-git.sh b/testing/latest-git.sh deleted file mode 100755 index 0f7a52a6b..000000000 --- a/testing/latest-git.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -# This is a script used in travis-ci to have latest git -set -ex -git clone git://github.com/git/git --depth 1 /tmp/git -pushd /tmp/git -make prefix=/tmp/git -j8 install -popd From fa8d02281373ec18c8463515c997291b6814e406 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 7 Feb 2020 08:32:39 -0800 Subject: [PATCH 340/967] Remove unnecessary forward annotations --- pre_commit/languages/conda.py | 2 +- pre_commit/languages/docker.py | 2 +- pre_commit/languages/docker_image.py | 2 +- pre_commit/languages/fail.py | 2 +- pre_commit/languages/golang.py | 2 +- pre_commit/languages/helpers.py | 4 ++-- pre_commit/languages/node.py | 2 +- pre_commit/languages/perl.py | 2 +- pre_commit/languages/pygrep.py | 2 +- pre_commit/languages/python.py | 4 ++-- pre_commit/languages/ruby.py | 2 +- pre_commit/languages/rust.py | 2 +- pre_commit/languages/script.py | 2 +- pre_commit/languages/swift.py | 2 +- pre_commit/languages/system.py | 2 +- 15 files changed, 17 insertions(+), 17 deletions(-) diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index 2c187e02f..071757a1f 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -72,7 +72,7 @@ def install_environment( def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 364a69967..921401f53 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -97,7 +97,7 @@ def docker_cmd() -> Tuple[str, ...]: # pragma: windows no cover def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: # pragma: windows no cover diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index 58da34c13..980c6ef33 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -13,7 +13,7 @@ def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: # pragma: windows no cover diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index 8cdc76c95..d2b02d23e 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -11,7 +11,7 @@ def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index cdcff0d58..91ade1e99 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -89,7 +89,7 @@ def install_environment( def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index ba96568cc..b5c95e522 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -72,7 +72,7 @@ def no_install( raise AssertionError('This type is not installable') -def target_concurrency(hook: 'Hook') -> int: +def target_concurrency(hook: Hook) -> int: if hook.require_serial or 'PRE_COMMIT_NO_CONCURRENCY' in os.environ: return 1 else: @@ -97,7 +97,7 @@ def _shuffled(seq: Sequence[str]) -> List[str]: def run_xargs( - hook: 'Hook', + hook: Hook, cmd: Tuple[str, ...], file_args: Sequence[str], **kwargs: Any, diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 481b0655f..787bcd720 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -85,7 +85,7 @@ def install_environment( def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py index f61815aa9..bbf550494 100644 --- a/pre_commit/languages/perl.py +++ b/pre_commit/languages/perl.py @@ -59,7 +59,7 @@ def install_environment( def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 68eb6e9be..40adba0f7 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -46,7 +46,7 @@ def _process_filename_at_once(pattern: Pattern[bytes], filename: str) -> int: def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 2a5cfe771..caa779489 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -145,7 +145,7 @@ def py_interface( ) -> Tuple[ Callable[[Prefix, str], ContextManager[None]], Callable[[Prefix, str], bool], - Callable[['Hook', Sequence[str], bool], Tuple[int, bytes]], + Callable[[Hook, Sequence[str], bool], Tuple[int, bytes]], Callable[[Prefix, str, Sequence[str]], None], ]: @contextlib.contextmanager @@ -168,7 +168,7 @@ def healthy(prefix: Prefix, language_version: str) -> bool: return retcode == 0 def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 828216fe1..26bd5be47 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -118,7 +118,7 @@ def install_environment( def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: # pragma: windows no cover diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index feb36847b..7ea3f5406 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -98,7 +98,7 @@ def install_environment( def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 1f6f354d5..a5e1365c0 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -11,7 +11,7 @@ def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 9f36b1521..a022bcee8 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -56,7 +56,7 @@ def install_environment( def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: # pragma: windows no cover diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 424e14fc4..139f45d13 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -12,7 +12,7 @@ def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: From cc45b5e57bb21d8f646d43a6aede0a7ac4e3ba46 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 7 Feb 2020 09:09:17 -0800 Subject: [PATCH 341/967] Improve git hook shebang creation --- pre_commit/commands/install_uninstall.py | 15 +++++--- tests/commands/install_uninstall_test.py | 45 +++++++++++++++++------- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index b2ccc5cf1..70118731d 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -30,6 +30,10 @@ CURRENT_HASH = '138fd403232d2ddd5efb44317e38bf03' TEMPLATE_START = '# start templated\n' TEMPLATE_END = '# end templated\n' +# Homebrew/homebrew-core#35825: be more timid about appropriate `PATH` +# #1312 os.defpath is too restrictive on BSD +POSIX_SEARCH_PATH = ('/usr/local/bin', '/usr/bin', '/bin') +SYS_EXE = os.path.basename(os.path.realpath(sys.executable)) def _hook_paths( @@ -51,20 +55,21 @@ def is_our_script(filename: str) -> bool: def shebang() -> str: if sys.platform == 'win32': - py = 'python' + py = SYS_EXE else: - # Homebrew/homebrew-core#35825: be more timid about appropriate `PATH` - path_choices = [p for p in os.defpath.split(os.pathsep) if p] exe_choices = [ f'python{sys.version_info[0]}.{sys.version_info[1]}', f'python{sys.version_info[0]}', ] - for path, exe in itertools.product(path_choices, exe_choices): + # avoid searching for bare `python` as it's likely to be python 2 + if SYS_EXE != 'python': + exe_choices.append(SYS_EXE) + for path, exe in itertools.product(POSIX_SEARCH_PATH, exe_choices): if os.access(os.path.join(path, exe), os.X_OK): py = exe break else: - py = 'python' + py = SYS_EXE return f'#!/usr/bin/env {py}' diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 6d4861490..e8e726163 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -4,6 +4,7 @@ from unittest import mock import pre_commit.constants as C +from pre_commit.commands import install_uninstall from pre_commit.commands.install_uninstall import CURRENT_HASH from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import install_hooks @@ -39,25 +40,36 @@ def test_is_previous_pre_commit(tmpdir): assert is_our_script(f.strpath) +def patch_platform(platform): + return mock.patch.object(sys, 'platform', platform) + + +def patch_lookup_path(path): + return mock.patch.object(install_uninstall, 'POSIX_SEARCH_PATH', path) + + +def patch_sys_exe(exe): + return mock.patch.object(install_uninstall, 'SYS_EXE', exe) + + def test_shebang_windows(): - with mock.patch.object(sys, 'platform', 'win32'): - assert shebang() == '#!/usr/bin/env python' + with patch_platform('win32'), patch_sys_exe('python.exe'): + assert shebang() == '#!/usr/bin/env python.exe' def test_shebang_posix_not_on_path(): - with mock.patch.object(sys, 'platform', 'posix'): - with mock.patch.object(os, 'defpath', ''): - assert shebang() == '#!/usr/bin/env python' + with patch_platform('posix'), patch_lookup_path(()): + with patch_sys_exe('python3.6'): + assert shebang() == '#!/usr/bin/env python3.6' def test_shebang_posix_on_path(tmpdir): exe = tmpdir.join(f'python{sys.version_info[0]}').ensure() make_executable(exe) - with mock.patch.object(sys, 'platform', 'posix'): - with mock.patch.object(os, 'defpath', tmpdir.strpath): - expected = f'#!/usr/bin/env python{sys.version_info[0]}' - assert shebang() == expected + with patch_platform('posix'), patch_lookup_path((tmpdir.strpath,)): + with patch_sys_exe('python'): + assert shebang() == f'#!/usr/bin/env python{sys.version_info[0]}' def test_install_pre_commit(in_git_dir, store): @@ -250,9 +262,18 @@ def _path_without_us(): def test_environment_not_sourced(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - # Patch the executable to simulate rming virtualenv - with mock.patch.object(sys, 'executable', '/does-not-exist'): - assert not install(C.CONFIG_FILE, store, hook_types=['pre-commit']) + assert not install(C.CONFIG_FILE, store, hook_types=['pre-commit']) + # simulate deleting the virtualenv by rewriting the exe + hook = os.path.join(path, '.git/hooks/pre-commit') + with open(hook) as f: + src = f.read() + src = re.sub( + '\nINSTALL_PYTHON =.*\n', + '\nINSTALL_PYTHON = "/dne"\n', + src, + ) + with open(hook, 'w') as f: + f.write(src) # Use a specific homedir to ignore --user installs homedir = tempdir_factory.get() From 5f64b1a255e8cdaf515762a1eca8b3bffa268be8 Mon Sep 17 00:00:00 2001 From: david <14880945+ddelange@users.noreply.github.com> Date: Fri, 14 Feb 2020 19:05:00 +0100 Subject: [PATCH 342/967] Add pre-commit badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 01d0d757a..98a6d00e0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ [![Build Status](https://dev.azure.com/asottile/asottile/_apis/build/status/pre-commit.pre-commit?branchName=master)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master) [![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/21/master.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master) +[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) ## pre-commit From 1c641b1c28ecc1005f46fdc76db4bbb0f67c82ac Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 18 Feb 2020 10:53:53 -0800 Subject: [PATCH 343/967] v2.1.0 --- CHANGELOG.md | 33 ++++++++++++++++++++++++++------- setup.cfg | 2 +- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ef3739b8..fe8e9fd13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,27 @@ +2.1.0 - 2020-02-18 +================== + +### Features +- Replace `aspy.yaml` with `sort_keys=False`. + - #1306 PR by @asottile. +- Add support for `perl`. + - #1303 PR by @scop. + +### Fixes +- Improve `.git/hooks/*` shebang creation when pythons are in `/usr/local/bin`. + - #1312 issue by @kbsezginel. + - #1319 PR by @asottile. + +### Misc. +- Add repository badge for pre-commit. + - [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) + - #1334 PR by @ddelange. + 2.0.1 - 2020-01-29 ================== ### Fixes -- Fix `ImportError` in python 3.6.0 / 3.6.1 for `typing.NoReturn` +- Fix `ImportError` in python 3.6.0 / 3.6.1 for `typing.NoReturn`. - #1302 PR by @asottile. 2.0.0 - 2020-01-28 @@ -412,7 +431,7 @@ - #881 issue by @henniss. - #912 PR by @asottile. -### Misc +### Misc. - Use `--no-gpg-sign` when running tests - #894 PR by @s0undt3ch. @@ -443,7 +462,7 @@ instead using `--no-document`. - #889 PR by @asottile. -### Misc +### Misc. - Use `--no-gpg-sign` when running tests - #885 PR by @s0undt3ch. @@ -532,7 +551,7 @@ - #772 issue by @asottile. - #803 PR by @mblayman. -### Misc +### Misc. - Improve travis-ci build times by caching rust / swift artifacts - #781 PR by @expobrain. - Test against python3.7 @@ -641,7 +660,7 @@ - #590 issue by @coldnight. - #711 PR by @asottile. -### Misc +### Misc. - test against swift 4.x - #709 by @theresama. @@ -685,7 +704,7 @@ - #200 issue by @asottile. - #685 PR by @asottile. -### Misc +### Misc. - internal reorganization of `PrefixedCommandRunner` -> `Prefix` - #684 PR by @asottile. - https-ify links. @@ -700,7 +719,7 @@ - Fix `local` golang repositories with `additional_dependencies`. - #679 #680 issue and PR by @asottile. -### Misc +### Misc. - Replace some string literals with constants - #678 PR by @revolter. diff --git a/setup.cfg b/setup.cfg index bf5c01c3d..3edb45b27 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.0.1 +version = 2.1.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 5258dce73b246cb8f1a31f769c0e0eb2352085dd Mon Sep 17 00:00:00 2001 From: Joey Espinosa Date: Sat, 22 Feb 2020 00:22:19 -0500 Subject: [PATCH 344/967] fix: catch missing arg if using {prepare-}commit-msg stage If using the prepare-commit-msg and commit-msg stages specifically (such as with the try-repo command), the `--commit-msg-filename` arg must be provided. [fixes #1336] chore: improve error message for hook stage check --- pre_commit/commands/run.py | 9 +++++++++ tests/commands/run_test.py | 19 +++++++++++-------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 95f8ab419..4f332ee97 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -306,6 +306,15 @@ def run( f'`git add {config_file}` to fix this.', ) return 1 + if ( + args.hook_stage in {'prepare-commit-msg', 'commit-msg'} and + not args.commit_msg_filename + ): + logger.error( + f'`--commit-msg-filename` is required for ' + f'`--hook-stage {args.hook_stage}`', + ) + return 1 # Expose origin / source as environment variables for hooks to consume if args.origin and args.source: diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 87eef2ec2..4519ad1aa 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -663,12 +663,7 @@ def test_stages(cap_out, store, repo_with_passing_hook): 'language': 'pygrep', 'stages': [stage], } - for i, stage in enumerate( - ( - 'commit', 'push', 'manual', 'prepare-commit-msg', - 'commit-msg', - ), 1, - ) + for i, stage in enumerate(('commit', 'push', 'manual'), 1) ], } add_config_to_repo(repo_with_passing_hook, config) @@ -686,8 +681,6 @@ def _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): @@ -819,6 +812,16 @@ def test_error_with_unstaged_config(cap_out, store, modified_config_repo): assert ret == 1 +def test_commit_msg_missing_filename(cap_out, store, repo_with_passing_hook): + args = run_opts(hook_stage='commit-msg') + ret, printed = _do_run(cap_out, store, repo_with_passing_hook, args) + assert ret == 1 + assert printed == ( + b'[ERROR] `--commit-msg-filename` is required for ' + b'`--hook-stage commit-msg`\n' + ) + + @pytest.mark.parametrize( 'opts', (run_opts(all_files=True), run_opts(files=[C.CONFIG_FILE])), ) From 18fa0042541b9fdbf65f6ea6285e4ef5a19e796f Mon Sep 17 00:00:00 2001 From: Andrew Hare Date: Thu, 20 Feb 2020 02:21:29 -0700 Subject: [PATCH 345/967] Add post-checkout --- pre_commit/commands/hook_impl.py | 7 +++++++ pre_commit/commands/run.py | 3 +++ pre_commit/constants.py | 2 +- pre_commit/main.py | 20 +++++++++++++++--- testing/util.py | 2 ++ tests/commands/hook_impl_test.py | 10 +++++++++ tests/commands/install_uninstall_test.py | 26 ++++++++++++++++++++++++ tests/commands/run_test.py | 10 +++++++++ tests/repository_test.py | 2 +- 9 files changed, 77 insertions(+), 5 deletions(-) diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index 0916c02bb..890cedb54 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -74,6 +74,7 @@ def _ns( remote_name: Optional[str] = None, remote_url: Optional[str] = None, commit_msg_filename: Optional[str] = None, + checkout_type: Optional[str] = None, ) -> argparse.Namespace: return argparse.Namespace( color=color, @@ -84,6 +85,7 @@ def _ns( remote_url=remote_url, commit_msg_filename=commit_msg_filename, all_files=all_files, + checkout_type=checkout_type, files=(), hook=None, verbose=False, @@ -157,6 +159,11 @@ def _run_ns( return _ns(hook_type, color, commit_msg_filename=args[0]) elif hook_type in {'pre-merge-commit', 'pre-commit'}: return _ns(hook_type, color) + elif hook_type == 'post-checkout': + return _ns( + hook_type, color, source=args[0], origin=args[1], + checkout_type=args[2], + ) else: raise AssertionError(f'unexpected hook type: {hook_type}') diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 95f8ab419..30970efdc 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -316,6 +316,9 @@ def run( environ['PRE_COMMIT_REMOTE_NAME'] = args.remote_name environ['PRE_COMMIT_REMOTE_URL'] = args.remote_url + if args.checkout_type: + environ['PRE_COMMIT_CHECKOUT_TYPE'] = args.checkout_type + with contextlib.ExitStack() as exit_stack: if stash: exit_stack.enter_context(staged_files_only(store.directory)) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 23622ecbf..e2b8e3aca 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -18,7 +18,7 @@ # `manual` is not invoked by any installed git hook. See #719 STAGES = ( 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', 'manual', - 'push', + 'post-checkout', 'push', ) DEFAULT = 'default' diff --git a/pre_commit/main.py b/pre_commit/main.py index 1d849c059..47dd73a52 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -79,7 +79,7 @@ def _add_hook_type_option(parser: argparse.ArgumentParser) -> None: parser.add_argument( '-t', '--hook-type', choices=( 'pre-commit', 'pre-merge-commit', 'pre-push', - 'prepare-commit-msg', 'commit-msg', + 'prepare-commit-msg', 'commit-msg', 'post-checkout', ), action=AppendReplaceDefault, default=['pre-commit'], @@ -92,11 +92,17 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: parser.add_argument('--verbose', '-v', action='store_true', default=False) parser.add_argument( '--origin', '-o', - help="The origin branch's commit_id when using `git push`.", + help=( + "The origin branch's commit_id when using `git push`. " + 'The ref of the previous HEAD when using `git checkout`.' + ), ) parser.add_argument( '--source', '-s', - help="The remote branch's commit_id when using `git push`.", + help=( + "The remote branch's commit_id when using `git push`. " + 'The ref of the new HEAD when using `git checkout`.' + ), ) parser.add_argument( '--commit-msg-filename', @@ -123,6 +129,14 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: '--files', nargs='*', default=[], help='Specific filenames to run hooks on.', ) + parser.add_argument( + '--checkout-type', + help=( + 'Indicates whether the checkout was a branch checkout ' + '(changing branches, flag=1) or a file checkout (retrieving a ' + 'file from the index, flag=0).' + ), + ) def _adjust_args_and_chdir(args: argparse.Namespace) -> None: diff --git a/testing/util.py b/testing/util.py index ce3206eb8..2875993cd 100644 --- a/testing/util.py +++ b/testing/util.py @@ -72,6 +72,7 @@ def run_opts( hook_stage='commit', show_diff_on_failure=False, commit_msg_filename='', + checkout_type='', ): # These are mutually exclusive assert not (all_files and files) @@ -88,6 +89,7 @@ def run_opts( hook_stage=hook_stage, show_diff_on_failure=show_diff_on_failure, commit_msg_filename=commit_msg_filename, + checkout_type=checkout_type, ) diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index 8fdbd0fa3..556ea363b 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -104,6 +104,16 @@ def test_run_ns_commit_msg(): assert ns.commit_msg_filename == '.git/COMMIT_MSG' +def test_run_ns_post_checkout(): + ns = hook_impl._run_ns('post-checkout', True, ('a', 'b', 'c'), b'') + assert ns is not None + assert ns.hook_stage == 'post-checkout' + assert ns.color is True + assert ns.source == 'a' + assert ns.origin == 'b' + assert ns.checkout_type == 'c' + + @pytest.fixture def push_example(tempdir_factory): src = git_dir(tempdir_factory) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index e8e726163..c76c303cb 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -20,6 +20,7 @@ from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo from testing.fixtures import remove_config_from_repo +from testing.fixtures import write_config from testing.util import cmd_output_mocked_pre_commit_home from testing.util import cwd from testing.util import git_commit @@ -725,6 +726,31 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): assert second_line.startswith('Must have "Signed off by:"...') +def test_post_checkout_integration(tempdir_factory, store): + path = git_dir(tempdir_factory) + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'post-checkout', + 'name': 'Post checkout', + 'entry': 'bash -c "echo ${PRE_COMMIT_ORIGIN}"', + 'language': 'system', + 'always_run': True, + 'verbose': True, + 'stages': ['post-checkout'], + }], + } + write_config(path, config) + with cwd(path): + cmd_output('git', 'add', '.') + git_commit() + install(C.CONFIG_FILE, store, hook_types=['post-checkout']) + retc, _, stderr = cmd_output('git', 'checkout', '-b', 'feature') + assert retc == 0 + _, head, _ = cmd_output('git', 'rev-parse', 'HEAD') + assert head in str(stderr) + + def test_prepare_commit_msg_integration_failing( failing_prepare_commit_msg_repo, tempdir_factory, store, ): diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 87eef2ec2..06ec2f3d1 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -18,6 +18,7 @@ from pre_commit.commands.run import filter_by_include_exclude from pre_commit.commands.run import run from pre_commit.util import cmd_output +from pre_commit.util import EnvironT from pre_commit.util import make_executable from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import add_config_to_repo @@ -466,6 +467,15 @@ def test_all_push_options_ok(cap_out, store, repo_with_passing_hook): assert b'Specify both --origin and --source.' not in printed +def test_checkout_type(cap_out, store, repo_with_passing_hook): + args = run_opts(origin='', source='', checkout_type='1') + environ: EnvironT = {} + ret, printed = _do_run( + cap_out, store, repo_with_passing_hook, args, environ, + ) + assert environ['PRE_COMMIT_CHECKOUT_TYPE'] == '1' + + def test_has_unmerged_paths(in_merge_conflict): assert _has_unmerged_paths() is True cmd_output('git', 'add', '.') diff --git a/tests/repository_test.py b/tests/repository_test.py index b745a9aa3..2d36df881 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -871,7 +871,7 @@ def test_manifest_hooks(tempdir_factory, store): require_serial=False, stages=( 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', - 'manual', 'push', + 'manual', 'post-checkout', 'push', ), types=['file'], verbose=False, From d35b00352fcdb33a51facbaf0bfe08da0fd3aca5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 23 Feb 2020 11:07:57 -0800 Subject: [PATCH 346/967] Make more readable --from-ref / --to-ref aliases for --source / --origin --- pre_commit/commands/hook_impl.py | 16 +++++----- pre_commit/commands/run.py | 20 +++++++----- pre_commit/git.py | 2 +- pre_commit/main.py | 53 ++++++++++++++++++-------------- testing/util.py | 8 ++--- tests/commands/hook_impl_test.py | 12 ++++---- tests/commands/run_test.py | 16 +++++----- tests/git_test.py | 6 ++-- 8 files changed, 72 insertions(+), 61 deletions(-) diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index 890cedb54..5ff4555ec 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -69,8 +69,8 @@ def _ns( color: bool, *, all_files: bool = False, - origin: Optional[str] = None, - source: Optional[str] = None, + from_ref: Optional[str] = None, + to_ref: Optional[str] = None, remote_name: Optional[str] = None, remote_url: Optional[str] = None, commit_msg_filename: Optional[str] = None, @@ -79,8 +79,8 @@ def _ns( return argparse.Namespace( color=color, hook_stage=hook_type.replace('pre-', ''), - origin=origin, - source=source, + from_ref=from_ref, + to_ref=to_ref, remote_name=remote_name, remote_url=remote_url, commit_msg_filename=commit_msg_filename, @@ -112,7 +112,7 @@ def _pre_push_ns( elif remote_sha != Z40 and _rev_exists(remote_sha): return _ns( 'pre-push', color, - origin=local_sha, source=remote_sha, + from_ref=remote_sha, to_ref=local_sha, remote_name=remote_name, remote_url=remote_url, ) else: @@ -139,7 +139,7 @@ def _pre_push_ns( source = subprocess.check_output(rev_cmd).decode().strip() return _ns( 'pre-push', color, - origin=local_sha, source=source, + from_ref=source, to_ref=local_sha, remote_name=remote_name, remote_url=remote_url, ) @@ -161,8 +161,8 @@ def _run_ns( return _ns(hook_type, color) elif hook_type == 'post-checkout': return _ns( - hook_type, color, source=args[0], origin=args[1], - checkout_type=args[2], + hook_type, color, + from_ref=args[0], to_ref=args[1], checkout_type=args[2], ) else: raise AssertionError(f'unexpected hook type: {hook_type}') diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 4f2ead783..43bcabad6 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -215,8 +215,8 @@ def _compute_cols(hooks: Sequence[Hook]) -> int: def _all_filenames(args: argparse.Namespace) -> Collection[str]: - if args.origin and args.source: - return git.get_changed_files(args.origin, args.source) + if args.from_ref and args.to_ref: + return git.get_changed_files(args.from_ref, args.to_ref) elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}: return (args.commit_msg_filename,) elif args.files: @@ -297,8 +297,8 @@ def run( if _has_unmerged_paths(): logger.error('Unmerged files. Resolve before committing.') return 1 - if bool(args.source) != bool(args.origin): - logger.error('Specify both --origin and --source.') + if bool(args.from_ref) != bool(args.to_ref): + logger.error('Specify both --from-ref and --to-ref.') return 1 if stash and _has_unstaged_config(config_file): logger.error( @@ -316,10 +316,14 @@ def run( ) return 1 - # Expose origin / source as environment variables for hooks to consume - if args.origin and args.source: - environ['PRE_COMMIT_ORIGIN'] = args.origin - environ['PRE_COMMIT_SOURCE'] = args.source + # Expose from-ref / to-ref as environment variables for hooks to consume + if args.from_ref and args.to_ref: + # legacy names + environ['PRE_COMMIT_ORIGIN'] = args.from_ref + environ['PRE_COMMIT_SOURCE'] = args.to_ref + # new names + environ['PRE_COMMIT_FROM_REF'] = args.from_ref + environ['PRE_COMMIT_TO_REF'] = args.to_ref if args.remote_name and args.remote_url: environ['PRE_COMMIT_REMOTE_NAME'] = args.remote_name diff --git a/pre_commit/git.py b/pre_commit/git.py index edde4b08d..7e757f247 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -129,7 +129,7 @@ def get_all_files() -> List[str]: return zsplit(cmd_output('git', 'ls-files', '-z')[1]) -def get_changed_files(new: str, old: str) -> List[str]: +def get_changed_files(old: str, new: str) -> List[str]: return zsplit( cmd_output( 'git', 'diff', '--name-only', '--no-ext-diff', '-z', diff --git a/pre_commit/main.py b/pre_commit/main.py index 47dd73a52..778334476 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -90,18 +90,42 @@ def _add_hook_type_option(parser: argparse.ArgumentParser) -> None: def _add_run_options(parser: argparse.ArgumentParser) -> None: parser.add_argument('hook', nargs='?', help='A single hook-id to run') parser.add_argument('--verbose', '-v', action='store_true', default=False) + mutex_group = parser.add_mutually_exclusive_group(required=False) + mutex_group.add_argument( + '--all-files', '-a', action='store_true', default=False, + help='Run on all the files in the repo.', + ) + mutex_group.add_argument( + '--files', nargs='*', default=[], + help='Specific filenames to run hooks on.', + ) + parser.add_argument( + '--show-diff-on-failure', action='store_true', + help='When hooks fail, run `git diff` directly afterward.', + ) + parser.add_argument( + '--hook-stage', choices=C.STAGES, default='commit', + help='The stage during which the hook is fired. One of %(choices)s', + ) parser.add_argument( - '--origin', '-o', + '--from-ref', '--source', '-s', help=( - "The origin branch's commit_id when using `git push`. " - 'The ref of the previous HEAD when using `git checkout`.' + '(for usage with `--from-ref`) -- this option represents the ' + 'destination ref in a `from_ref...to_ref` diff expression. ' + 'For `pre-push` hooks, this represents the branch being pushed. ' + 'For `post-checkout` hooks, this represents the branch that is ' + 'now checked out.' ), ) parser.add_argument( - '--source', '-s', + '--to-ref', '--origin', '-o', help=( - "The remote branch's commit_id when using `git push`. " - 'The ref of the new HEAD when using `git checkout`.' + '(for usage with `--to-ref`) -- this option represents the ' + 'original ref in a `from_ref...to_ref` diff expression. ' + 'For `pre-push` hooks, this represents the branch you are pushing ' + 'to. ' + 'For `post-checkout` hooks, this represents the branch which was ' + 'previously checked out.' ), ) parser.add_argument( @@ -112,23 +136,6 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: '--remote-name', help='Remote name used by `git push`.', ) parser.add_argument('--remote-url', help='Remote url used by `git push`.') - parser.add_argument( - '--hook-stage', choices=C.STAGES, default='commit', - help='The stage during which the hook is fired. One of %(choices)s', - ) - parser.add_argument( - '--show-diff-on-failure', action='store_true', - help='When hooks fail, run `git diff` directly afterward.', - ) - mutex_group = parser.add_mutually_exclusive_group(required=False) - mutex_group.add_argument( - '--all-files', '-a', action='store_true', default=False, - help='Run on all the files in the repo.', - ) - mutex_group.add_argument( - '--files', nargs='*', default=[], - help='Specific filenames to run hooks on.', - ) parser.add_argument( '--checkout-type', help=( diff --git a/testing/util.py b/testing/util.py index 2875993cd..439bee794 100644 --- a/testing/util.py +++ b/testing/util.py @@ -65,8 +65,8 @@ def run_opts( color=False, verbose=False, hook=None, - origin='', - source='', + from_ref='', + to_ref='', remote_name='', remote_url='', hook_stage='commit', @@ -82,8 +82,8 @@ def run_opts( color=color, verbose=verbose, hook=hook, - origin=origin, - source=source, + from_ref=from_ref, + to_ref=to_ref, remote_name=remote_name, remote_url=remote_url, hook_stage=hook_stage, diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index 556ea363b..032fa8fa8 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -109,8 +109,8 @@ def test_run_ns_post_checkout(): assert ns is not None assert ns.hook_stage == 'post-checkout' assert ns.color is True - assert ns.source == 'a' - assert ns.origin == 'b' + assert ns.from_ref == 'a' + assert ns.to_ref == 'b' assert ns.checkout_type == 'c' @@ -140,8 +140,8 @@ def test_run_ns_pre_push_updating_branch(push_example): assert ns.color is False assert ns.remote_name == 'origin' assert ns.remote_url == src - assert ns.source == src_head - assert ns.origin == clone_head + assert ns.from_ref == src_head + assert ns.to_ref == clone_head assert ns.all_files is False @@ -154,8 +154,8 @@ def test_run_ns_pre_push_new_branch(push_example): ns = hook_impl._run_ns('pre-push', False, args, stdin) assert ns is not None - assert ns.source == src_head - assert ns.origin == clone_head + assert ns.from_ref == src_head + assert ns.to_ref == clone_head def test_run_ns_pre_push_new_branch_existing_rev(push_example): diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index f48f71b3a..63129ff5b 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -446,29 +446,29 @@ def test_hook_verbose_enabled(cap_out, store, repo_with_passing_hook): @pytest.mark.parametrize( - ('origin', 'source'), (('master', ''), ('', 'master')), + ('from_ref', 'to_ref'), (('master', ''), ('', 'master')), ) -def test_origin_source_error_msg_error( - cap_out, store, repo_with_passing_hook, origin, source, +def test_from_ref_to_ref_error_msg_error( + cap_out, store, repo_with_passing_hook, from_ref, to_ref, ): - args = run_opts(origin=origin, source=source) + args = run_opts(from_ref=from_ref, to_ref=to_ref) ret, printed = _do_run(cap_out, store, repo_with_passing_hook, args) assert ret == 1 - assert b'Specify both --origin and --source.' in printed + assert b'Specify both --from-ref and --to-ref.' in printed def test_all_push_options_ok(cap_out, store, repo_with_passing_hook): args = run_opts( - origin='master', source='master', + from_ref='master', to_ref='master', remote_name='origin', remote_url='https://example.com/repo', ) ret, printed = _do_run(cap_out, store, repo_with_passing_hook, args) assert ret == 0 - assert b'Specify both --origin and --source.' not in printed + assert b'Specify both --from-ref and --to-ref.' not in printed def test_checkout_type(cap_out, store, repo_with_passing_hook): - args = run_opts(origin='', source='', checkout_type='1') + args = run_opts(from_ref='', to_ref='', checkout_type='1') environ: EnvironT = {} ret, printed = _do_run( cap_out, store, repo_with_passing_hook, args, environ, diff --git a/tests/git_test.py b/tests/git_test.py index 4a5bfb9be..e73a6f240 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -100,11 +100,11 @@ def test_get_changed_files(in_git_dir): in_git_dir.join('b.txt').ensure() cmd_output('git', 'add', '.') git_commit() - files = git.get_changed_files('HEAD', 'HEAD^') + files = git.get_changed_files('HEAD^', 'HEAD') assert files == ['a.txt', 'b.txt'] # files changed in source but not in origin should not be returned - files = git.get_changed_files('HEAD^', 'HEAD') + files = git.get_changed_files('HEAD', 'HEAD^') assert files == [] @@ -142,7 +142,7 @@ def test_staged_files_non_ascii(non_ascii_repo): def test_changed_files_non_ascii(non_ascii_repo): - ret = git.get_changed_files('HEAD', 'HEAD^') + ret = git.get_changed_files('HEAD^', 'HEAD') assert ret == ['интервью'] From 53052fe019d58712e0f733a532a3aa940d1057b2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 23 Feb 2020 11:36:12 -0800 Subject: [PATCH 347/967] Ensure files aren't passed to post-checkout hooks --- pre_commit/commands/run.py | 6 ++-- tests/commands/install_uninstall_test.py | 43 +++++++++++++++--------- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 43bcabad6..2f745782e 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -215,10 +215,12 @@ def _compute_cols(hooks: Sequence[Hook]) -> int: def _all_filenames(args: argparse.Namespace) -> Collection[str]: - if args.from_ref and args.to_ref: - return git.get_changed_files(args.from_ref, args.to_ref) + if args.hook_stage == 'post-checkout': # no files for post-checkout + return () elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}: return (args.commit_msg_filename,) + elif args.from_ref and args.to_ref: + return git.get_changed_files(args.from_ref, args.to_ref) elif args.files: return args.files elif args.all_files: diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index c76c303cb..2f6c49fb0 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -4,6 +4,7 @@ from unittest import mock import pre_commit.constants as C +from pre_commit import git from pre_commit.commands import install_uninstall from pre_commit.commands.install_uninstall import CURRENT_HASH from pre_commit.commands.install_uninstall import install @@ -728,27 +729,39 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): def test_post_checkout_integration(tempdir_factory, store): path = git_dir(tempdir_factory) - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'post-checkout', - 'name': 'Post checkout', - 'entry': 'bash -c "echo ${PRE_COMMIT_ORIGIN}"', - 'language': 'system', - 'always_run': True, - 'verbose': True, - 'stages': ['post-checkout'], - }], - } + config = [ + { + 'repo': 'local', + 'hooks': [{ + 'id': 'post-checkout', + 'name': 'Post checkout', + 'entry': 'bash -c "echo ${PRE_COMMIT_TO_REF}"', + 'language': 'system', + 'always_run': True, + 'verbose': True, + 'stages': ['post-checkout'], + }], + }, + {'repo': 'meta', 'hooks': [{'id': 'identity'}]}, + ] write_config(path, config) with cwd(path): cmd_output('git', 'add', '.') git_commit() + + # add a file only on `feature`, it should not be passed to hooks + cmd_output('git', 'checkout', '-b', 'feature') + open('some_file', 'a').close() + cmd_output('git', 'add', '.') + git_commit() + cmd_output('git', 'checkout', 'master') + install(C.CONFIG_FILE, store, hook_types=['post-checkout']) - retc, _, stderr = cmd_output('git', 'checkout', '-b', 'feature') + retc, _, stderr = cmd_output('git', 'checkout', 'feature') + assert stderr is not None assert retc == 0 - _, head, _ = cmd_output('git', 'rev-parse', 'HEAD') - assert head in str(stderr) + assert git.head_rev(path) in stderr + assert 'some_file' not in stderr def test_prepare_commit_msg_integration_failing( From 1b93e26b5829165d12a13dee3aa1df3fbb33642e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 23 Feb 2020 14:53:03 -0800 Subject: [PATCH 348/967] Fix test coverage --- tests/commands/run_test.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 63129ff5b..f8e882367 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -22,6 +22,7 @@ from pre_commit.util import make_executable from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import add_config_to_repo +from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo from testing.fixtures import modify_config from testing.fixtures import read_config @@ -709,6 +710,27 @@ def test_commit_msg_hook(cap_out, store, commit_msg_repo): ) +def test_post_checkout_hook(cap_out, store, tempdir_factory): + path = git_dir(tempdir_factory) + config = { + 'repo': 'meta', 'hooks': [ + {'id': 'identity', 'stages': ['post-checkout']}, + ], + } + add_config_to_repo(path, config) + + with cwd(path): + _test_run( + cap_out, + store, + path, + {'hook_stage': 'post-checkout'}, + expected_outputs=[b'identity...'], + expected_ret=0, + stage=False, + ) + + def test_prepare_commit_msg_hook(cap_out, store, prepare_commit_msg_repo): filename = '.git/COMMIT_EDITMSG' with open(filename, 'w') as f: From 081f3028ee9f47dfd7efa8f210980b9aba5eb605 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 24 Feb 2020 09:02:19 -0800 Subject: [PATCH 349/967] Temporarily restore python 3.6.0 support --- .pre-commit-config.yaml | 5 --- pre_commit/commands/autoupdate.py | 57 +++++++++++++++++-------------- pre_commit/envcontext.py | 6 +++- pre_commit/hook.py | 51 +++++++++++++++------------ pre_commit/prefix.py | 23 +++++++++---- 5 files changed, 83 insertions(+), 59 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 23c19961c..ecac7002f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -43,11 +43,6 @@ repos: rev: v1.6.0 hooks: - id: setup-cfg-fmt -- repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.761 - hooks: - - id: mypy - exclude: ^testing/resources/ - repo: meta hooks: - id: check-hooks-apply diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 5a9a9880c..9cf251eb7 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -31,32 +31,39 @@ class RevInfo(NamedTuple): rev: str frozen: Optional[str] - @classmethod - def from_config(cls, config: Dict[str, Any]) -> 'RevInfo': - return cls(config['repo'], config['rev'], None) - def update(self, tags_only: bool, freeze: bool) -> 'RevInfo': - if tags_only: - tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags', '--abbrev=0') - else: - tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags', '--exact') - - 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) +@classmethod +def RevInfo_from_config(cls, config: Dict[str, Any]) -> 'RevInfo': + return cls(config['repo'], config['rev'], None) + + +def RevInfo_update(self, tags_only: bool, freeze: bool) -> 'RevInfo': + if tags_only: + tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags', '--abbrev=0') + else: + tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags', '--exact') + + with tmpdir() as tmp: + git.init_repo(tmp, self.repo) + cmd_output_b('git', 'fetch', 'origin', 'HEAD', '--tags', cwd=tmp) + + try: + rev = cmd_output(*tag_cmd, cwd=tmp)[1].strip() + except CalledProcessError: + cmd = ('git', 'rev-parse', 'FETCH_HEAD') + rev = cmd_output(*cmd, cwd=tmp)[1].strip() + + frozen = None + if freeze: + exact = cmd_output('git', 'rev-parse', rev, cwd=tmp)[1].strip() + if exact != rev: + rev, frozen = exact, rev + return self._replace(rev=rev, frozen=frozen) + + +# python 3.6.0 does not support methods on `typing.NamedTuple` +RevInfo.from_config = RevInfo_from_config +RevInfo.update = RevInfo_update class RepositoryCannotBeUpdatedError(RuntimeError): diff --git a/pre_commit/envcontext.py b/pre_commit/envcontext.py index 16d3d15e3..946131867 100644 --- a/pre_commit/envcontext.py +++ b/pre_commit/envcontext.py @@ -19,7 +19,11 @@ class _Unset(enum.Enum): class Var(NamedTuple): name: str - default: str = '' + default: str + + +# python3.6.0: `typing.NamedTuple` did not support defaults +Var.__new__.__defaults__ = ('',) SubstitutionT = Tuple[Union[str, Var], ...] diff --git a/pre_commit/hook.py b/pre_commit/hook.py index b65ac42b0..e4de95519 100644 --- a/pre_commit/hook.py +++ b/pre_commit/hook.py @@ -35,29 +35,38 @@ class Hook(NamedTuple): stages: Sequence[str] verbose: bool - @property - def cmd(self) -> Tuple[str, ...]: - return (*shlex.split(self.entry), *self.args) - - @property - def install_key(self) -> Tuple[Prefix, str, str, Tuple[str, ...]]: - return ( - self.prefix, - self.language, - self.language_version, - tuple(self.additional_dependencies), + +@property +def Hook_cmd(self: Hook) -> Tuple[str, ...]: + return (*shlex.split(self.entry), *self.args) + + +@property +def Hook_install_key(self) -> Tuple[Prefix, str, str, Tuple[str, ...]]: + return ( + self.prefix, + self.language, + self.language_version, + tuple(self.additional_dependencies), + ) + + +@classmethod +def Hook_create(cls, src: str, prefix: Prefix, dct: Dict[str, Any]) -> 'Hook': + # TODO: have cfgv do this (?) + extra_keys = set(dct) - _KEYS + if extra_keys: + logger.warning( + f'Unexpected key(s) present on {src} => {dct["id"]}: ' + f'{", ".join(sorted(extra_keys))}', ) + return cls(src=src, prefix=prefix, **{k: dct[k] for k in _KEYS}) + - @classmethod - def create(cls, src: str, prefix: Prefix, dct: Dict[str, Any]) -> 'Hook': - # TODO: have cfgv do this (?) - extra_keys = set(dct) - _KEYS - if extra_keys: - logger.warning( - f'Unexpected key(s) present on {src} => {dct["id"]}: ' - f'{", ".join(sorted(extra_keys))}', - ) - return cls(src=src, prefix=prefix, **{k: dct[k] for k in _KEYS}) +# python 3.6.0 does not support methods on `typing.NamedTuple` +Hook.cmd = Hook_cmd +Hook.install_key = Hook_install_key +Hook.create = Hook_create _KEYS = frozenset(set(Hook._fields) - {'src', 'prefix'}) diff --git a/pre_commit/prefix.py b/pre_commit/prefix.py index 0e3ebbd89..23316c3f7 100644 --- a/pre_commit/prefix.py +++ b/pre_commit/prefix.py @@ -6,12 +6,21 @@ class Prefix(NamedTuple): prefix_dir: str - def path(self, *parts: str) -> str: - return os.path.normpath(os.path.join(self.prefix_dir, *parts)) - def exists(self, *parts: str) -> bool: - return os.path.exists(self.path(*parts)) +def Prefix_path(self, *parts: str) -> str: + return os.path.normpath(os.path.join(self.prefix_dir, *parts)) - def star(self, end: str) -> Tuple[str, ...]: - paths = os.listdir(self.prefix_dir) - return tuple(path for path in paths if path.endswith(end)) + +def Prefix_exists(self, *parts: str) -> bool: + return os.path.exists(self.path(*parts)) + + +def Prefix_star(self, end: str) -> Tuple[str, ...]: + paths = os.listdir(self.prefix_dir) + return tuple(path for path in paths if path.endswith(end)) + + +# python 3.6.0 does not support methods on `typing.NamedTuple` +Prefix.path = Prefix_path +Prefix.exists = Prefix_exists +Prefix.star = Prefix_star From ccf84fb698cf6cc028411a9a2f62331e8676567d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 24 Feb 2020 09:04:36 -0800 Subject: [PATCH 350/967] v2.1.1 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe8e9fd13..6aa783b44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +2.1.1 - 2020-02-24 +================== + +### Fixes +- Temporarily restore python 3.6.0 support (broken in 2.0.0) + - reported by @obestwalter. + - 081f3028 by @asottile. + 2.1.0 - 2020-02-18 ================== diff --git a/setup.cfg b/setup.cfg index 3edb45b27..9f3ae7dff 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.1.0 +version = 2.1.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From b2b5676698fc6b0ec0269914e409933b48c19e9f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 24 Feb 2020 09:34:55 -0800 Subject: [PATCH 351/967] Drop python 3.6.0 support (broken NamedTuple) --- .pre-commit-config.yaml | 10 +++++----- setup.cfg | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 23c19961c..c2df486eb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.4.0 + rev: v2.5.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -15,17 +15,17 @@ repos: rev: 3.7.9 hooks: - id: flake8 - additional_dependencies: [flake8-typing-imports==1.5.0] + additional_dependencies: [flake8-typing-imports==1.6.0] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.4.4 + rev: v1.5 hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v1.21.0 + rev: v2.1.1 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v1.25.3 + rev: v2.0.1 hooks: - id: pyupgrade args: [--py36-plus] diff --git a/setup.cfg b/setup.cfg index 9f3ae7dff..4536e9e7b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,7 +30,7 @@ install_requires = virtualenv>=15.2 importlib-metadata;python_version<"3.8" importlib-resources;python_version<"3.7" -python_requires = >=3.6 +python_requires = >=3.6.1 [options.entry_points] console_scripts = From 67c1beb3229cb929a9e52a48cdf1c04028452e77 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Feb 2020 14:00:31 -0800 Subject: [PATCH 352/967] Use covdefaults to handle coveragerc --- .coveragerc | 37 ------------------------ azure-pipelines.yml | 3 -- pre_commit/color.py | 2 +- pre_commit/commands/install_uninstall.py | 2 +- pre_commit/file_lock.py | 2 +- pre_commit/languages/docker.py | 18 ++++++------ pre_commit/languages/docker_image.py | 2 +- pre_commit/languages/node.py | 2 +- pre_commit/languages/ruby.py | 12 ++++---- pre_commit/languages/swift.py | 8 ++--- pre_commit/parse_shebang.py | 2 +- pre_commit/util.py | 2 +- requirements-dev.txt | 3 +- setup.cfg | 4 +++ tests/commands/install_uninstall_test.py | 2 -- tests/languages/python_test.py | 4 +-- tests/parse_shebang_test.py | 2 +- tests/repository_test.py | 14 ++++----- tox.ini | 2 +- 19 files changed, 42 insertions(+), 81 deletions(-) delete mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 7cf6cfae3..000000000 --- a/.coveragerc +++ /dev/null @@ -1,37 +0,0 @@ -[run] -branch = True -source = . -omit = - .tox/* - /usr/* - setup.py - # Don't complain if non-runnable code isn't run - */__main__.py - pre_commit/resources/* - -[report] -show_missing = True -skip_covered = True -exclude_lines = - # Have to re-enable the standard pragma - \#\s*pragma: no cover - # We optionally substitute this - ${COVERAGE_IGNORE_WINDOWS} - - # Don't complain if tests don't hit defensive assertion code: - ^\s*raise AssertionError\b - ^\s*raise NotImplementedError\b - ^\s*return NotImplemented\b - ^\s*raise$ - - # Ignore typing-related things - ^if (False|TYPE_CHECKING): - : \.\.\.$ - - # Don't complain if non-runnable code isn't run: - ^if __name__ == ['"]__main__['"]:$ - -[html] -directory = coverage-html - -# vim:ft=dosini diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c51b4a5f7..9b385b4c1 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -18,9 +18,6 @@ jobs: parameters: toxenvs: [py37] os: windows - additional_variables: - COVERAGE_IGNORE_WINDOWS: '# pragma: windows no cover' - TOX_TESTENV_PASSENV: COVERAGE_IGNORE_WINDOWS pre_test: - powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts" displayName: Add conda to PATH diff --git a/pre_commit/color.py b/pre_commit/color.py index caf4cb082..5fa70421b 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -50,7 +50,7 @@ def bool_errcheck(result, func, args): terminal_supports_color = False else: terminal_supports_color = True -else: # pragma: windows no cover +else: # pragma: win32 no cover terminal_supports_color = True RED = '\033[41m' diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 70118731d..c8b7633b6 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -46,7 +46,7 @@ def _hook_paths( def is_our_script(filename: str) -> bool: - if not os.path.exists(filename): # pragma: windows no cover (symlink) + if not os.path.exists(filename): # pragma: win32 no cover (symlink) return False with open(filename) as f: contents = f.read() diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index 241923c7f..ff0dc5e64 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -47,7 +47,7 @@ def _locked( # before closing a file or exiting the program." # TODO: https://github.com/python/typeshed/pull/3607 msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region) # type: ignore -else: # pragma: windows no cover +else: # pragma: win32 no cover import fcntl @contextlib.contextmanager diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 921401f53..f4495847d 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -17,16 +17,16 @@ healthy = helpers.basic_healthy -def md5(s: str) -> str: # pragma: windows no cover +def md5(s: str) -> str: # pragma: win32 no cover return hashlib.md5(s.encode()).hexdigest() -def docker_tag(prefix: Prefix) -> str: # pragma: windows no cover +def docker_tag(prefix: Prefix) -> str: # pragma: win32 no cover md5sum = md5(os.path.basename(prefix.prefix_dir)).lower() return f'pre-commit-{md5sum}' -def docker_is_running() -> bool: # pragma: windows no cover +def docker_is_running() -> bool: # pragma: win32 no cover try: cmd_output_b('docker', 'ps') except CalledProcessError: @@ -35,7 +35,7 @@ def docker_is_running() -> bool: # pragma: windows no cover return True -def assert_docker_available() -> None: # pragma: windows no cover +def assert_docker_available() -> None: # pragma: win32 no cover assert docker_is_running(), ( 'Docker is either not running or not configured in this environment' ) @@ -45,7 +45,7 @@ def build_docker_image( prefix: Prefix, *, pull: bool, -) -> None: # pragma: windows no cover +) -> None: # pragma: win32 no cover cmd: Tuple[str, ...] = ( 'docker', 'build', '--tag', docker_tag(prefix), @@ -60,7 +60,7 @@ def build_docker_image( def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], -) -> None: # pragma: windows no cover +) -> None: # pragma: win32 no cover helpers.assert_version_default('docker', version) helpers.assert_no_additional_deps('docker', additional_dependencies) assert_docker_available() @@ -76,14 +76,14 @@ def install_environment( os.mkdir(directory) -def get_docker_user() -> str: # pragma: windows no cover +def get_docker_user() -> str: # pragma: win32 no cover try: return f'{os.getuid()}:{os.getgid()}' except AttributeError: return '1000:1000' -def docker_cmd() -> Tuple[str, ...]: # pragma: windows no cover +def docker_cmd() -> Tuple[str, ...]: # pragma: win32 no cover return ( 'docker', 'run', '--rm', @@ -100,7 +100,7 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: # pragma: windows no cover +) -> Tuple[int, bytes]: # pragma: win32 no cover assert_docker_available() # Rebuild the docker image in case it has gone missing, as many people do # automated cleanup of docker images. diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index 980c6ef33..0c51df628 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -16,7 +16,7 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: # pragma: windows no cover +) -> Tuple[int, bytes]: # pragma: win32 no cover assert_docker_available() cmd = docker_cmd() + hook.cmd return helpers.run_xargs(hook, cmd, file_args, color=color) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 787bcd720..79ff807a5 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -35,7 +35,7 @@ def get_env_patch(venv: str) -> PatchesT: elif sys.platform == 'win32': # pragma: no cover install_prefix = bin_dir(venv) lib_dir = 'Scripts' - else: # pragma: windows no cover + else: # pragma: win32 no cover install_prefix = venv lib_dir = 'lib' return ( diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 26bd5be47..61241f855 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -25,7 +25,7 @@ def get_env_patch( venv: str, language_version: str, -) -> PatchesT: # pragma: windows no cover +) -> PatchesT: # pragma: win32 no cover patches: PatchesT = ( ('GEM_HOME', os.path.join(venv, 'gems')), ('RBENV_ROOT', venv), @@ -43,7 +43,7 @@ def get_env_patch( return patches -@contextlib.contextmanager # pragma: windows no cover +@contextlib.contextmanager # pragma: win32 no cover def in_env( prefix: Prefix, language_version: str, @@ -64,7 +64,7 @@ def _extract_resource(filename: str, dest: str) -> None: def _install_rbenv( prefix: Prefix, version: str = C.DEFAULT, -) -> None: # pragma: windows no cover +) -> None: # pragma: win32 no cover directory = helpers.environment_dir(ENVIRONMENT_DIR, version) _extract_resource('rbenv.tar.gz', prefix.path('.')) @@ -80,7 +80,7 @@ def _install_rbenv( def _install_ruby( prefix: Prefix, version: str, -) -> None: # pragma: windows no cover +) -> None: # pragma: win32 no cover try: helpers.run_setup_cmd(prefix, ('rbenv', 'download', version)) except CalledProcessError: # pragma: no cover (usually find with download) @@ -90,7 +90,7 @@ def _install_ruby( def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], -) -> None: # pragma: windows no cover +) -> None: # pragma: win32 no cover additional_dependencies = tuple(additional_dependencies) directory = helpers.environment_dir(ENVIRONMENT_DIR, version) with clean_path_on_failure(prefix.path(directory)): @@ -121,6 +121,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: # pragma: windows no cover +) -> Tuple[int, bytes]: # pragma: win32 no cover with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index a022bcee8..66aadc8b2 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -21,12 +21,12 @@ BUILD_CONFIG = 'release' -def get_env_patch(venv: str) -> PatchesT: # pragma: windows no cover +def get_env_patch(venv: str) -> PatchesT: # pragma: win32 no cover bin_path = os.path.join(venv, BUILD_DIR, BUILD_CONFIG) return (('PATH', (bin_path, os.pathsep, Var('PATH'))),) -@contextlib.contextmanager # pragma: windows no cover +@contextlib.contextmanager # pragma: win32 no cover def in_env(prefix: Prefix) -> Generator[None, None, None]: envdir = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), @@ -37,7 +37,7 @@ def in_env(prefix: Prefix) -> Generator[None, None, None]: def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], -) -> None: # pragma: windows no cover +) -> None: # pragma: win32 no cover helpers.assert_version_default('swift', version) helpers.assert_no_additional_deps('swift', additional_dependencies) directory = prefix.path( @@ -59,6 +59,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: # pragma: windows no cover +) -> Tuple[int, bytes]: # pragma: win32 no cover with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index 7b9a05828..d344a1dab 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -59,7 +59,7 @@ def _error(msg: str) -> 'NoReturn': _error('is a directory') elif not os.path.isfile(orig): _error('not found') - elif not os.access(orig, os.X_OK): # pragma: windows no cover + elif not os.access(orig, os.X_OK): # pragma: win32 no cover _error('is not executable') else: return orig diff --git a/pre_commit/util.py b/pre_commit/util.py index 65775710d..7da41c446 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -149,7 +149,7 @@ def cmd_output(*cmd: str, **kwargs: Any) -> Tuple[int, str, Optional[str]]: return returncode, stdout, stderr -if os.name != 'nt': # pragma: windows no cover +if os.name != 'nt': # pragma: win32 no cover from os import openpty import termios diff --git a/requirements-dev.txt b/requirements-dev.txt index 9dfea92d0..d6a13dc43 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,4 @@ --e . - +covdefaults coverage pytest pytest-env diff --git a/setup.cfg b/setup.cfg index 4536e9e7b..8cdf960e3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -52,6 +52,10 @@ exclude = [bdist_wheel] universal = True +[coverage:run] +plugins = covdefaults +omit = pre_commit/resources/* + [mypy] check_untyped_defs = true disallow_any_generics = true diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 2f6c49fb0..66b91903b 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -25,7 +25,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_windows def test_is_not_script(): @@ -823,7 +822,6 @@ def test_prepare_commit_msg_legacy( assert 'Signed off by: ' in f.read() -@xfailif_windows # pragma: windows no cover (once AP has git 2.24) def test_pre_merge_commit_integration(tempdir_factory, store): expected = re.compile( r'^\[INFO\] Initializing environment for .+\n' diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 19890d746..245c73a08 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -11,10 +11,10 @@ def test_norm_version_expanduser(): home = os.path.expanduser('~') - if os.name == 'nt': # pragma: no cover (nt) + if os.name == 'nt': # pragma: nt cover path = r'~\python343' expected_path = fr'{home}\python343' - else: # pragma: windows no cover + else: # pragma: nt no cover path = '~/.pyenv/versions/3.4.3/bin/python' expected_path = f'{home}/.pyenv/versions/3.4.3/bin/python' result = python.norm_version(path) diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index 62eb81e5e..0bb19c78b 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -93,7 +93,7 @@ def test_normexe_does_not_exist_sep(): @pytest.mark.xfail(os.name == 'nt', reason='posix only') -def test_normexe_not_executable(tmpdir): # pragma: windows no cover +def test_normexe_not_executable(tmpdir): # pragma: win32 no cover tmpdir.join('exe').ensure() with tmpdir.as_cwd(), pytest.raises(OSError) as excinfo: parse_shebang.normexe('./exe') diff --git a/tests/repository_test.py b/tests/repository_test.py index 2d36df881..df7e7d1bc 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -197,7 +197,7 @@ def test_versioned_python_hook(tempdir_factory, store): ) -@skipif_cant_run_docker # pragma: windows no cover +@skipif_cant_run_docker # pragma: win32 no cover def test_run_a_docker_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'docker_hooks_repo', @@ -206,7 +206,7 @@ def test_run_a_docker_hook(tempdir_factory, store): ) -@skipif_cant_run_docker # pragma: windows no cover +@skipif_cant_run_docker # pragma: win32 no cover def test_run_a_docker_hook_with_entry_args(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'docker_hooks_repo', @@ -215,7 +215,7 @@ def test_run_a_docker_hook_with_entry_args(tempdir_factory, store): ) -@skipif_cant_run_docker # pragma: windows no cover +@skipif_cant_run_docker # pragma: win32 no cover def test_run_a_failing_docker_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'docker_hooks_repo', @@ -226,7 +226,7 @@ def test_run_a_failing_docker_hook(tempdir_factory, store): ) -@skipif_cant_run_docker # pragma: windows no cover +@skipif_cant_run_docker # pragma: win32 no cover @pytest.mark.parametrize('hook_id', ('echo-entrypoint', 'echo-cmd')) def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): _test_hook_repo( @@ -297,7 +297,7 @@ def test_system_hook_with_spaces(tempdir_factory, store): ) -@skipif_cant_run_swift # pragma: windows no cover +@skipif_cant_run_swift # pragma: win32 no cover def test_swift_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'swift_hooks_repo', @@ -514,7 +514,7 @@ def test_additional_dependencies_roll_forward(tempdir_factory, store): assert 'mccabe' not in cmd_output('pip', 'freeze', '-l')[1] -@xfailif_windows_no_ruby # pragma: windows no cover +@xfailif_windows_no_ruby # pragma: win32 no cover def test_additional_ruby_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'ruby_hooks_repo') config = make_config_from_repo(path) @@ -758,7 +758,7 @@ def local_python_config(): return {'repo': 'local', 'hooks': hooks} -@pytest.mark.xfail( # pragma: windows no cover +@pytest.mark.xfail( # pragma: win32 no cover sys.platform == 'win32', reason='microsoft/azure-pipelines-image-generation#989', ) diff --git a/tox.ini b/tox.ini index 7fd0bf6ae..d9f9420c9 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,7 @@ passenv = HOME LOCALAPPDATA RUSTUP_HOME commands = coverage erase coverage run -m pytest {posargs:tests} - coverage report --fail-under 100 + coverage report pre-commit install [testenv:pre-commit] From 01be1713cf12f932e2740f378cb39005b035fa1d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 10 Mar 2020 09:02:51 -0700 Subject: [PATCH 353/967] Don't crash on un-stringable exceptions --- pre_commit/error_handler.py | 16 +++++++++++++--- tests/error_handler_test.py | 17 +++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 0ea7ed3fb..b095ba2d7 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -14,14 +14,24 @@ class FatalError(RuntimeError): pass +def _exception_to_bytes(exc: BaseException) -> bytes: + with contextlib.suppress(TypeError): + return bytes(exc) # type: ignore + with contextlib.suppress(Exception): + return str(exc).encode() + return f''.encode() + + def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None: - error_msg = f'{msg}: {type(exc).__name__}: {exc}' - output.write_line(error_msg) + error_msg = f'{msg}: {type(exc).__name__}: '.encode() + error_msg += _exception_to_bytes(exc) + output.write_line_b(error_msg) log_path = os.path.join(Store().directory, 'pre-commit.log') output.write_line(f'Check the log at {log_path}') with open(log_path, 'wb') as log: _log_line = functools.partial(output.write_line, stream=log) + _log_line_b = functools.partial(output.write_line_b, stream=log) _log_line('### version information') _log_line() @@ -39,7 +49,7 @@ def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None: _log_line('### error information') _log_line() _log_line('```') - _log_line(error_msg) + _log_line_b(error_msg) _log_line('```') _log_line() _log_line('```') diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index a8626f73f..833bb8f83 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -6,6 +6,7 @@ import pytest from pre_commit import error_handler +from pre_commit.util import CalledProcessError from testing.util import cmd_output_mocked_pre_commit_home @@ -135,6 +136,22 @@ def test_error_handler_non_ascii_exception(mock_store_dir): raise ValueError('☃') +def test_error_handler_non_utf8_exception(mock_store_dir): + with pytest.raises(SystemExit): + with error_handler.error_handler(): + raise CalledProcessError(1, ('exe',), 0, b'error: \xa0\xe1', b'') + + +def test_error_handler_non_stringable_exception(mock_store_dir): + class C(Exception): + def __str__(self): + raise RuntimeError('not today!') + + with pytest.raises(SystemExit): + with error_handler.error_handler(): + raise C() + + def test_error_handler_no_tty(tempdir_factory): pre_commit_home = tempdir_factory.get() ret, out, _ = cmd_output_mocked_pre_commit_home( From 7a49309035502ba5fd0b321571697e42b2f31763 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 12 Mar 2020 09:18:45 -0700 Subject: [PATCH 354/967] mark a python environment as unhealthy if python goes missing --- pre_commit/languages/python.py | 6 ++++-- tests/languages/python_test.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index caa779489..5073a8bce 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -158,10 +158,12 @@ def in_env( yield def healthy(prefix: Prefix, language_version: str) -> bool: + envdir = helpers.environment_dir(_dir, language_version) + exe_name = 'python.exe' if sys.platform == 'win32' else 'python' + py_exe = prefix.path(bin_dir(envdir), exe_name) with in_env(prefix, language_version): retcode, _, _ = cmd_output_b( - 'python', '-c', - 'import ctypes, datetime, io, os, ssl, weakref', + py_exe, '-c', 'import ctypes, datetime, io, os, ssl, weakref', cwd='/', retcode=None, ) diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 245c73a08..34c6c7fc5 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -59,3 +59,17 @@ def test_healthy_types_py_in_cwd(tmpdir): # even if a `types.py` file exists, should still be healthy tmpdir.join('types.py').ensure() assert python.healthy(prefix, C.DEFAULT) is True + + +def test_healthy_python_goes_missing(tmpdir): + with tmpdir.as_cwd(): + prefix = tmpdir.join('prefix').ensure_dir() + prefix.join('setup.py').write('import setuptools; setuptools.setup()') + prefix = Prefix(str(prefix)) + python.install_environment(prefix, C.DEFAULT, ()) + + exe_name = 'python' if sys.platform != 'win32' else 'python.exe' + py_exe = prefix.path(python.bin_dir('py_env-default'), exe_name) + os.remove(py_exe) + + assert python.healthy(prefix, C.DEFAULT) is False From 03617b2f98517404ce756776066ed6b039e5ac3a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 12 Mar 2020 10:48:35 -0700 Subject: [PATCH 355/967] Don't crash out on OSErrors in subprocess calls --- pre_commit/error_handler.py | 12 ++---------- pre_commit/util.py | 28 ++++++++++++++++++++++++---- tests/util_test.py | 13 +++++++++++++ 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index b095ba2d7..b2321ae0d 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -8,23 +8,15 @@ import pre_commit.constants as C from pre_commit import output from pre_commit.store import Store +from pre_commit.util import force_bytes class FatalError(RuntimeError): pass -def _exception_to_bytes(exc: BaseException) -> bytes: - with contextlib.suppress(TypeError): - return bytes(exc) # type: ignore - with contextlib.suppress(Exception): - return str(exc).encode() - return f''.encode() - - def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None: - error_msg = f'{msg}: {type(exc).__name__}: '.encode() - error_msg += _exception_to_bytes(exc) + error_msg = f'{msg}: {type(exc).__name__}: '.encode() + force_bytes(exc) output.write_line_b(error_msg) log_path = os.path.join(Store().directory, 'pre-commit.log') output.write_line(f'Check the log at {log_path}') diff --git a/pre_commit/util.py b/pre_commit/util.py index 7da41c446..2db579a5f 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -43,6 +43,14 @@ def yaml_dump(o: Any) -> str: ) +def force_bytes(exc: Any) -> bytes: + with contextlib.suppress(TypeError): + return bytes(exc) + with contextlib.suppress(Exception): + return str(exc).encode() + return f''.encode() + + @contextlib.contextmanager def clean_path_on_failure(path: str) -> Generator[None, None, None]: """Cleans up the directory on an exceptional failure.""" @@ -120,6 +128,10 @@ def _setdefault_kwargs(kwargs: Dict[str, Any]) -> None: kwargs.setdefault(arg, subprocess.PIPE) +def _oserror_to_output(e: OSError) -> Tuple[int, bytes, None]: + return 1, force_bytes(e).rstrip(b'\n') + b'\n', None + + def cmd_output_b( *cmd: str, retcode: Optional[int] = 0, @@ -132,9 +144,13 @@ def cmd_output_b( except parse_shebang.ExecutableNotFoundError as e: returncode, stdout_b, stderr_b = e.to_output() else: - proc = subprocess.Popen(cmd, **kwargs) - stdout_b, stderr_b = proc.communicate() - returncode = proc.returncode + try: + proc = subprocess.Popen(cmd, **kwargs) + except OSError as e: + returncode, stdout_b, stderr_b = _oserror_to_output(e) + else: + stdout_b, stderr_b = proc.communicate() + returncode = proc.returncode if retcode is not None and retcode != returncode: raise CalledProcessError(returncode, cmd, retcode, stdout_b, stderr_b) @@ -205,7 +221,11 @@ def cmd_output_p( with open(os.devnull) as devnull, Pty() as pty: assert pty.r is not None kwargs.update({'stdin': devnull, 'stdout': pty.w, 'stderr': pty.w}) - proc = subprocess.Popen(cmd, **kwargs) + try: + proc = subprocess.Popen(cmd, **kwargs) + except OSError as e: + return _oserror_to_output(e) + pty.close_w() buf = b'' diff --git a/tests/util_test.py b/tests/util_test.py index 9f75f6a5b..01afbd4bf 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -9,6 +9,7 @@ from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_p +from pre_commit.util import make_executable from pre_commit.util import parse_version from pre_commit.util import rmtree from pre_commit.util import tmpdir @@ -92,6 +93,18 @@ def test_cmd_output_exe_not_found_bytes(fn): assert out == b'Executable `dne` not found' +@pytest.mark.parametrize('fn', (cmd_output_b, cmd_output_p)) +def test_cmd_output_no_shebang(tmpdir, fn): + f = tmpdir.join('f').ensure() + make_executable(f) + + # previously this raised `OSError` -- the output is platform specific + ret, out, _ = fn(str(f), retcode=None, stderr=subprocess.STDOUT) + assert ret == 1 + assert isinstance(out, bytes) + assert out.endswith(b'\n') + + def test_parse_version(): assert parse_version('0.0') == parse_version('0.0') assert parse_version('0.1') > parse_version('0.0') From 1e0db9c2c8983f1f8e969686fa5cb3d5ef21ea91 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 12 Mar 2020 12:27:54 -0700 Subject: [PATCH 356/967] Fix help description for --from-ref and --to-ref --- pre_commit/main.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 778334476..790b34773 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -111,21 +111,21 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: '--from-ref', '--source', '-s', help=( '(for usage with `--from-ref`) -- this option represents the ' - 'destination ref in a `from_ref...to_ref` diff expression. ' - 'For `pre-push` hooks, this represents the branch being pushed. ' - 'For `post-checkout` hooks, this represents the branch that is ' - 'now checked out.' + 'original ref in a `from_ref...to_ref` diff expression. ' + 'For `pre-push` hooks, this represents the branch you are pushing ' + 'to. ' + 'For `post-checkout` hooks, this represents the branch that was ' + 'previously checked out.' ), ) parser.add_argument( '--to-ref', '--origin', '-o', help=( '(for usage with `--to-ref`) -- this option represents the ' - 'original ref in a `from_ref...to_ref` diff expression. ' - 'For `pre-push` hooks, this represents the branch you are pushing ' - 'to. ' - 'For `post-checkout` hooks, this represents the branch which was ' - 'previously checked out.' + 'destination ref in a `from_ref...to_ref` diff expression. ' + 'For `pre-push` hooks, this represents the branch being pushed. ' + 'For `post-checkout` hooks, this represents the branch that is ' + 'now checked out.' ), ) parser.add_argument( From 30d3bb29900cf7caa6624edbaf3faf37a11b07f3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 12 Mar 2020 12:37:15 -0700 Subject: [PATCH 357/967] v2.2.0 --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6aa783b44..9a6892c33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,32 @@ +2.2.0 - 2020-03-12 +================== + +### Features +- Add support for the `post-checkout` hook + - #1210 issue by @domenkozar. + - #1339 PR by @andrewhare. +- Add more readable `--from-ref` / `--to-ref` aliases for `--source` / + `--origin` + - #1343 PR by @asottile. + +### Fixes +- Make sure that `--commit-msg-filename` is passed for `commit-msg` / + `prepare-commit-msg`. + - #1336 PR by @particledecay. + - #1341 PR by @particledecay. +- Fix crash when installation error is un-decodable bytes + - #1358 issue by @Guts. + - #1359 PR by @asottile. +- Fix python `healthy()` check when `python` executable goes missing. + - #1363 PR by @asottile. +- Fix crash when script executables are missing shebangs. + - #1350 issue by @chriselion. + - #1364 PR by @asottile. + +### Misc. +- pre-commit now requires python>=3.6.1 (previously 3.6.0) + - #1346 PR by @asottile. + 2.1.1 - 2020-02-24 ================== diff --git a/setup.cfg b/setup.cfg index 8cdf960e3..a02fab181 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.1.1 +version = 2.2.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From bb6f1efe63c168d9393d520bd60e16c991a57059 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 13 Mar 2020 23:26:51 -0700 Subject: [PATCH 358/967] Fix issue link --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a6892c33..050dfd5be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ### Features - Add support for the `post-checkout` hook - - #1210 issue by @domenkozar. + - #1120 issue by @domenkozar. - #1339 PR by @andrewhare. - Add more readable `--from-ref` / `--to-ref` aliases for `--source` / `--origin` From 23d5b78fdb8b9e853f88110552ea90c319e69f5f Mon Sep 17 00:00:00 2001 From: KYLE ZHU Date: Thu, 19 Mar 2020 16:56:08 -0400 Subject: [PATCH 359/967] Don't use --user when running docker on windows --- pre_commit/languages/docker.py | 8 ++++---- tests/languages/docker_test.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index f4495847d..4091492cc 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -76,18 +76,18 @@ def install_environment( os.mkdir(directory) -def get_docker_user() -> str: # pragma: win32 no cover +def get_docker_user() -> Tuple[str, ...]: # pragma: win32 no cover try: - return f'{os.getuid()}:{os.getgid()}' + return ('-u', f'{os.getuid()}:{os.getgid()}') except AttributeError: - return '1000:1000' + return () def docker_cmd() -> Tuple[str, ...]: # pragma: win32 no cover return ( 'docker', 'run', '--rm', - '-u', get_docker_user(), + *get_docker_user(), # https://docs.docker.com/engine/reference/commandline/run/#mount-volumes-from-container-volumes-from # The `Z` option tells Docker to label the content with a private # unshared label. Only the current container can use a private volume. diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 171a3f732..b65b2235a 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -20,4 +20,4 @@ def invalid_attribute(): getuid=invalid_attribute, getgid=invalid_attribute, ): - assert docker.get_docker_user() == '1000:1000' + assert docker.get_docker_user() == () From 605b39f617c18143cfd4a25ada2611ce9f96a68c Mon Sep 17 00:00:00 2001 From: zjeuhpiung liu Date: Fri, 27 Mar 2020 17:33:16 +0800 Subject: [PATCH 360/967] fix CJK characters width in output --- pre_commit/commands/run.py | 12 +++++++++--- tests/commands/run_test.py | 12 ++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 2f745782e..8c8401ce0 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -6,6 +6,7 @@ import re import subprocess import time +import unicodedata from typing import Any from typing import Collection from typing import Dict @@ -33,8 +34,13 @@ logger = logging.getLogger('pre_commit') +def _len_cjk(msg: str) -> int: + widths = {'A': 1, 'F': 2, 'H': 1, 'N': 1, 'Na': 1, 'W': 2} + return sum(widths[unicodedata.east_asian_width(c)] for c in msg) + + def _start_msg(*, start: str, cols: int, end_len: int) -> str: - dots = '.' * (cols - len(start) - end_len - 1) + dots = '.' * (cols - _len_cjk(start) - end_len - 1) return f'{start}{dots}' @@ -47,7 +53,7 @@ def _full_msg( use_color: bool, postfix: str = '', ) -> str: - dots = '.' * (cols - len(start) - len(postfix) - len(end_msg) - 1) + dots = '.' * (cols - _len_cjk(start) - len(postfix) - len(end_msg) - 1) end = color.format_color(end_msg, end_color, use_color) return f'{start}{dots}{postfix}{end}\n' @@ -206,7 +212,7 @@ def _compute_cols(hooks: Sequence[Hook]) -> int: Hook name...(no files to check) Skipped """ if hooks: - name_len = max(len(hook.name) for hook in hooks) + name_len = max(_len_cjk(hook.name) for hook in hooks) else: name_len = 0 diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index f8e882367..c51bcff0a 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -52,6 +52,18 @@ def test_full_msg(): assert ret == 'start......end\n' +def test_full_msg_with_cjk(): + ret = _full_msg( + start='啊あ아', + end_msg='end', + end_color='', + use_color=False, + cols=15, + ) + # 5 dots: 15 - 6 - 3 - 1 + assert ret == '啊あ아.....end\n' + + def test_full_msg_with_color(): ret = _full_msg( start='start', From 9fc5a9316e482aff148a1ba248c71e92cb6c9d53 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 2 Apr 2020 00:08:20 -0700 Subject: [PATCH 361/967] support colors on windows during git better --- pre_commit/color.py | 10 +++++----- tests/color_test.py | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pre_commit/color.py b/pre_commit/color.py index 5fa70421b..eb906b78f 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -11,7 +11,7 @@ def _enable() -> None: from ctypes.wintypes import DWORD from ctypes.wintypes import HANDLE - STD_OUTPUT_HANDLE = -11 + STD_ERROR_HANDLE = -12 ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 def bool_errcheck(result, func, args): @@ -40,9 +40,9 @@ def bool_errcheck(result, func, args): # # More info on the escape sequences supported: # https://msdn.microsoft.com/en-us/library/windows/desktop/mt638032(v=vs.85).aspx - stdout = GetStdHandle(STD_OUTPUT_HANDLE) - flags = GetConsoleMode(stdout) - SetConsoleMode(stdout, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING) + stderr = GetStdHandle(STD_ERROR_HANDLE) + flags = GetConsoleMode(stderr) + SetConsoleMode(stderr, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING) try: _enable() @@ -90,7 +90,7 @@ def use_color(setting: str) -> bool: return ( setting == 'always' or ( setting == 'auto' and - sys.stdout.isatty() and + sys.stderr.isatty() and terminal_supports_color and os.getenv('TERM') != 'dumb' ) diff --git a/tests/color_test.py b/tests/color_test.py index 98b39c1e1..5cd226a9e 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -29,26 +29,26 @@ def test_use_color_always(): def test_use_color_no_tty(): - with mock.patch.object(sys.stdout, 'isatty', return_value=False): + with mock.patch.object(sys.stderr, 'isatty', return_value=False): assert use_color('auto') is False def test_use_color_tty_with_color_support(): - with mock.patch.object(sys.stdout, 'isatty', return_value=True): + with mock.patch.object(sys.stderr, 'isatty', return_value=True): with mock.patch('pre_commit.color.terminal_supports_color', True): with envcontext.envcontext((('TERM', envcontext.UNSET),)): assert use_color('auto') is True def test_use_color_tty_without_color_support(): - with mock.patch.object(sys.stdout, 'isatty', return_value=True): + with mock.patch.object(sys.stderr, 'isatty', return_value=True): with mock.patch('pre_commit.color.terminal_supports_color', False): with envcontext.envcontext((('TERM', envcontext.UNSET),)): assert use_color('auto') is False def test_use_color_dumb_term(): - with mock.patch.object(sys.stdout, 'isatty', return_value=True): + with mock.patch.object(sys.stderr, 'isatty', return_value=True): with mock.patch('pre_commit.color.terminal_supports_color', True): with envcontext.envcontext((('TERM', 'dumb'),)): assert use_color('auto') is False From 0f528544b5a5d74744a2f9271a215098c12982f1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 7 Apr 2020 18:57:58 -0700 Subject: [PATCH 362/967] Default to `language_version: system` if node and npm are installed --- .pre-commit-config.yaml | 14 +++++------ pre_commit/languages/node.py | 16 +++++++++++- tests/languages/node_test.py | 47 ++++++++++++++++++++++++++++++++++++ tests/repository_test.py | 15 +++++++++--- 4 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 tests/languages/node_test.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c2df486eb..b51417d1d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,34 +17,34 @@ repos: - id: flake8 additional_dependencies: [flake8-typing-imports==1.6.0] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.5 + rev: v1.5.1 hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.1.1 + rev: v2.2.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.0.1 + rev: v2.1.0 hooks: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/asottile/reorder_python_imports - rev: v1.9.0 + rev: v2.1.0 hooks: - id: reorder-python-imports args: [--py3-plus] - repo: https://github.com/asottile/add-trailing-comma - rev: v1.5.0 + rev: v2.0.1 hooks: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.6.0 + rev: v1.8.2 hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.761 + rev: v0.770 hooks: - id: mypy exclude: ^testing/resources/ diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 79ff807a5..9b636d30d 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -1,4 +1,5 @@ import contextlib +import functools import os import sys from typing import Generator @@ -6,6 +7,7 @@ from typing import Tuple import pre_commit.constants as C +from pre_commit import parse_shebang from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var @@ -18,10 +20,22 @@ from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'node_env' -get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy +@functools.lru_cache(maxsize=1) +def get_default_version() -> str: + # nodeenv does not yet support `-n system` on windows + if sys.platform == 'win32': + return C.DEFAULT + # if node is already installed, we can save a bunch of setup time by + # using the installed version + elif all(parse_shebang.find_executable(exe) for exe in ('node', 'npm')): + return 'system' + else: + return C.DEFAULT + + def _envdir(prefix: Prefix, version: str) -> str: directory = helpers.environment_dir(ENVIRONMENT_DIR, version) return prefix.path(directory) diff --git a/tests/languages/node_test.py b/tests/languages/node_test.py new file mode 100644 index 000000000..fd300469a --- /dev/null +++ b/tests/languages/node_test.py @@ -0,0 +1,47 @@ +import sys +from unittest import mock + +import pytest + +import pre_commit.constants as C +from pre_commit import parse_shebang +from pre_commit.languages.node import get_default_version + + +ACTUAL_GET_DEFAULT_VERSION = get_default_version.__wrapped__ + + +@pytest.fixture +def is_linux(): + with mock.patch.object(sys, 'platform', 'linux'): + yield + + +@pytest.fixture +def is_win32(): + with mock.patch.object(sys, 'platform', 'win32'): + yield + + +@pytest.fixture +def find_exe_mck(): + with mock.patch.object(parse_shebang, 'find_executable') as mck: + yield mck + + +@pytest.mark.usefixtures('is_linux') +def test_sets_system_when_node_and_npm_are_available(find_exe_mck): + find_exe_mck.return_value = '/path/to/exe' + assert ACTUAL_GET_DEFAULT_VERSION() == 'system' + + +@pytest.mark.usefixtures('is_linux') +def test_uses_default_when_node_and_npm_are_not_available(find_exe_mck): + find_exe_mck.return_value = None + assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT + + +@pytest.mark.usefixtures('is_win32') +def test_sets_default_on_windows(find_exe_mck): + find_exe_mck.return_value = '/path/to/exe' + assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT diff --git a/tests/repository_test.py b/tests/repository_test.py index df7e7d1bc..3c7a63727 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -131,9 +131,9 @@ def test_python_hook(tempdir_factory, store): def test_python_hook_default_version(tempdir_factory, store): # make sure that this continues to work for platforms where default # language detection does not work - with mock.patch.object( - python, 'get_default_version', return_value=C.DEFAULT, - ): + returns_default = mock.Mock(return_value=C.DEFAULT) + lang = languages['python']._replace(get_default_version=returns_default) + with mock.patch.dict(languages, python=lang): test_python_hook(tempdir_factory, store) @@ -243,6 +243,15 @@ def test_run_a_node_hook(tempdir_factory, store): ) +def test_run_a_node_hook_default_version(tempdir_factory, store): + # make sure that this continues to work for platforms where node is not + # installed at the system + returns_default = mock.Mock(return_value=C.DEFAULT) + lang = languages['node']._replace(get_default_version=returns_default) + with mock.patch.dict(languages, node=lang): + test_run_a_node_hook(tempdir_factory, store) + + def test_run_versioned_node_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'node_versioned_hooks_repo', From 80a59db0944faea03956f9de6b1dd33ac16c0b4f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 15 Apr 2020 12:30:44 -0700 Subject: [PATCH 363/967] validate argument length as part of hook-impl --- pre_commit/commands/hook_impl.py | 31 +++++++++++++++++++++- tests/commands/hook_impl_test.py | 45 ++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index 5ff4555ec..4843fc77e 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -147,15 +147,44 @@ def _pre_push_ns( return None +_EXPECTED_ARG_LENGTH_BY_HOOK = { + 'commit-msg': 1, + 'post-checkout': 3, + 'pre-commit': 0, + 'pre-merge-commit': 0, + 'pre-push': 2, +} + + +def _check_args_length(hook_type: str, args: Sequence[str]) -> None: + if hook_type == 'prepare-commit-msg': + if len(args) < 1 or len(args) > 3: + raise SystemExit( + f'hook-impl for {hook_type} expected 1, 2, or 3 arguments ' + f'but got {len(args)}: {args}', + ) + elif hook_type in _EXPECTED_ARG_LENGTH_BY_HOOK: + expected = _EXPECTED_ARG_LENGTH_BY_HOOK[hook_type] + if len(args) != expected: + arguments_s = 'argument' if expected == 1 else 'arguments' + raise SystemExit( + f'hook-impl for {hook_type} expected {expected} {arguments_s} ' + f'but got {len(args)}: {args}', + ) + else: + raise AssertionError(f'unexpected hook type: {hook_type}') + + def _run_ns( hook_type: str, color: bool, args: Sequence[str], stdin: bytes, ) -> Optional[argparse.Namespace]: + _check_args_length(hook_type, args) if hook_type == 'pre-push': return _pre_push_ns(color, args, stdin) - elif hook_type in {'prepare-commit-msg', 'commit-msg'}: + elif hook_type in {'commit-msg', 'prepare-commit-msg'}: return _ns(hook_type, color, commit_msg_filename=args[0]) elif hook_type in {'pre-merge-commit', 'pre-commit'}: return _ns(hook_type, color) diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index 032fa8fa8..ddf65b776 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -89,6 +89,51 @@ def call(*_, **__): call() +@pytest.mark.parametrize( + ('hook_type', 'args'), + ( + ('pre-commit', []), + ('pre-merge-commit', []), + ('pre-push', ['branch_name', 'remote_name']), + ('commit-msg', ['.git/COMMIT_EDITMSG']), + ('post-checkout', ['old_head', 'new_head', '1']), + # multiple choices for commit-editmsg + ('prepare-commit-msg', ['.git/COMMIT_EDITMSG']), + ('prepare-commit-msg', ['.git/COMMIT_EDITMSG', 'message']), + ('prepare-commit-msg', ['.git/COMMIT_EDITMSG', 'commit', 'deadbeef']), + ), +) +def test_check_args_length_ok(hook_type, args): + hook_impl._check_args_length(hook_type, args) + + +def test_check_args_length_error_too_many_plural(): + with pytest.raises(SystemExit) as excinfo: + hook_impl._check_args_length('pre-commit', ['run', '--all-files']) + msg, = excinfo.value.args + assert msg == ( + 'hook-impl for pre-commit expected 0 arguments but got 2: ' + "['run', '--all-files']" + ) + + +def test_check_args_length_error_too_many_singluar(): + with pytest.raises(SystemExit) as excinfo: + hook_impl._check_args_length('commit-msg', []) + msg, = excinfo.value.args + assert msg == 'hook-impl for commit-msg expected 1 argument but got 0: []' + + +def test_check_args_length_prepare_commit_msg_error(): + with pytest.raises(SystemExit) as excinfo: + hook_impl._check_args_length('prepare-commit-msg', []) + msg, = excinfo.value.args + assert msg == ( + 'hook-impl for prepare-commit-msg expected 1, 2, or 3 arguments ' + 'but got 0: []' + ) + + def test_run_ns_pre_commit(): ns = hook_impl._run_ns('pre-commit', True, (), b'') assert ns is not None From 522e82b7b704d39f52252c3dab2df8767879f230 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 17 Apr 2020 07:41:11 -0700 Subject: [PATCH 364/967] Allow pip to be upgradable on windows --- pre_commit/languages/python.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 5073a8bce..85d828107 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -182,8 +182,8 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - additional_dependencies = tuple(additional_dependencies) directory = helpers.environment_dir(_dir, version) + install = ('python', '-mpip', 'install', '.', *additional_dependencies) env_dir = prefix.path(directory) with clean_path_on_failure(env_dir): @@ -193,9 +193,7 @@ def install_environment( python = os.path.realpath(sys.executable) _make_venv(env_dir, python) with in_env(prefix, version): - helpers.run_setup_cmd( - prefix, ('pip', 'install', '.') + additional_dependencies, - ) + helpers.run_setup_cmd(prefix, install) return in_env, healthy, run_hook, install_environment From 13d528c56937916bc676ea747aa6bfef94ff1e12 Mon Sep 17 00:00:00 2001 From: Lukasz Boldys Date: Sat, 18 Apr 2020 18:15:00 +0200 Subject: [PATCH 365/967] Preserve line ending when running autoupdate --- pre_commit/commands/autoupdate.py | 4 ++-- tests/commands/autoupdate_test.py | 39 +++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 5a9a9880c..8c9fdd7d0 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -93,7 +93,7 @@ def _original_lines( retry: bool = False, ) -> Tuple[List[str], List[int]]: """detect `rev:` lines or reformat the file""" - with open(path) as f: + with open(path, newline='') as f: original = f.read() lines = original.splitlines(True) @@ -126,7 +126,7 @@ def _write_new_config(path: str, rev_infos: List[Optional[RevInfo]]) -> None: comment = match[4] lines[idx] = f'{match[1]}rev:{match[2]}{new_rev}{comment}{match[5]}' - with open(path, 'w') as f: + with open(path, 'w', newline='') as f: f.write(''.join(lines)) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 2c7b2f1fa..25161d18c 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -263,6 +263,45 @@ def test_does_not_reformat(tmpdir, out_of_date, store): assert cfg.read() == expected +def test_does_not_change_mixed_endlines_read(up_to_date, tmpdir, store): + fmt = ( + 'repos:\n' + '- repo: {}\n' + ' rev: {} # definitely the version I want!\r\n' + ' hooks:\r\n' + ' - id: foo\n' + ' # These args are because reasons!\r\n' + ' args: [foo, bar, baz]\r\n' + ) + cfg = tmpdir.join(C.CONFIG_FILE) + + expected = fmt.format(up_to_date, git.head_rev(up_to_date)).encode() + cfg.write_binary(expected) + + assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + assert cfg.read_binary() == expected + + +def test_does_not_change_mixed_endlines_write(tmpdir, out_of_date, store): + fmt = ( + 'repos:\n' + '- repo: {}\n' + ' rev: {} # definitely the version I want!\r\n' + ' hooks:\r\n' + ' - id: foo\n' + ' # These args are because reasons!\r\n' + ' args: [foo, bar, baz]\r\n' + ) + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write_binary( + fmt.format(out_of_date.path, out_of_date.original_rev).encode(), + ) + + assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + expected = fmt.format(out_of_date.path, out_of_date.head_rev).encode() + assert cfg.read_binary() == expected + + def test_loses_formatting_when_not_detectable(out_of_date, store, tmpdir): """A best-effort attempt is made at updating rev without rewriting formatting. When the original formatting cannot be detected, this From bcff73c9cc76f614d41933974b8f4b4d52e19251 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 22 Apr 2020 14:15:52 -0700 Subject: [PATCH 366/967] v2.3.0 --- CHANGELOG.md | 25 +++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 050dfd5be..5b833190e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +2.3.0 - 2020-04-22 +================== + +### Features +- Calculate character width using `east_asian_width` + - #1378 PR by @sophgn. +- Use `language_version: system` by default for `node` hooks if `node` / `npm` + are globally installed. + - #1388 PR by @asottile. + +### Fixes +- No longer use a hard-coded user id for docker hooks on windows + - #1371 PR by @killuazhu. +- Fix colors on windows during `git commit` + - #1381 issue by @Cielquan. + - #1382 PR by @asottile. +- Produce readable error message for incorrect argument count to `hook-impl` + - #1394 issue by @pip9ball. + - #1395 PR by @asottile. +- Fix installations which involve an upgrade of `pip` on windows + - #1398 issue by @xiaohuazi123. + - #1399 PR by @asottile. +- Preserve line endings in `pre-commit autoupdate` + - #1402 PR by @utek. + 2.2.0 - 2020-03-12 ================== diff --git a/setup.cfg b/setup.cfg index a02fab181..2e69d5032 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.2.0 +version = 2.3.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 3ff133c1662f1054f9bb6b34e748cbd468908d3a Mon Sep 17 00:00:00 2001 From: Robin Modisch Date: Sat, 25 Apr 2020 01:39:22 +0200 Subject: [PATCH 367/967] add instructions to activate virtualenvs on windows --- CONTRIBUTING.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2b83c8232..d70a89dd9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,8 @@ This is useful for running specific tests. The easiest way to set this up is to run: 1. `tox --devenv venv` (note: requires tox>=3.13) -2. `. venv/bin/activate` +2. `. venv/bin/activate` (or follow the [activation instructions] for your + platform) This will create and put you into a virtualenv which has an editable installation of pre-commit. Hack away! Running `pre-commit` will reflect @@ -144,3 +145,5 @@ This is usually the easiest to implement, most of them look the same as the `node` hook implementation: https://github.com/pre-commit/pre-commit/blob/160238220f022035c8ef869c9a8642f622c02118/pre_commit/languages/node.py#L72-L74 + +[activation instructions]: https://virtualenv.pypa.io/en/latest/user_guide.html#activators From 26adf1d560d24a4c5764c6cb42e66dc2c876b18f Mon Sep 17 00:00:00 2001 From: ModischFabrications Date: Sat, 25 Apr 2020 01:21:12 +0200 Subject: [PATCH 368/967] add support for post-commit --- pre_commit/commands/hook_impl.py | 3 ++- pre_commit/commands/run.py | 3 ++- pre_commit/constants.py | 4 ++-- pre_commit/main.py | 2 +- tests/commands/hook_impl_test.py | 8 ++++++++ tests/commands/install_uninstall_test.py | 26 ++++++++++++++++++++++++ tests/repository_test.py | 2 +- 7 files changed, 42 insertions(+), 6 deletions(-) diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index 4843fc77e..d0e226f8c 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -150,6 +150,7 @@ def _pre_push_ns( _EXPECTED_ARG_LENGTH_BY_HOOK = { 'commit-msg': 1, 'post-checkout': 3, + 'post-commit': 0, 'pre-commit': 0, 'pre-merge-commit': 0, 'pre-push': 2, @@ -186,7 +187,7 @@ def _run_ns( return _pre_push_ns(color, args, stdin) elif hook_type in {'commit-msg', 'prepare-commit-msg'}: return _ns(hook_type, color, commit_msg_filename=args[0]) - elif hook_type in {'pre-merge-commit', 'pre-commit'}: + elif hook_type in {'post-commit', 'pre-merge-commit', 'pre-commit'}: return _ns(hook_type, color) elif hook_type == 'post-checkout': return _ns( diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 8c8401ce0..8a9352d4a 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -221,7 +221,8 @@ def _compute_cols(hooks: Sequence[Hook]) -> int: def _all_filenames(args: argparse.Namespace) -> Collection[str]: - if args.hook_stage == 'post-checkout': # no files for post-checkout + # these hooks do not operate on files + if args.hook_stage in {'post-checkout', 'post-commit'}: return () elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}: return (args.commit_msg_filename,) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index e2b8e3aca..5150fdcf4 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -17,8 +17,8 @@ # `manual` is not invoked by any installed git hook. See #719 STAGES = ( - 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', 'manual', - 'post-checkout', 'push', + 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', + 'post-commit', 'manual', 'post-checkout', 'push', ) DEFAULT = 'default' diff --git a/pre_commit/main.py b/pre_commit/main.py index 790b34773..874eb53a5 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -79,7 +79,7 @@ def _add_hook_type_option(parser: argparse.ArgumentParser) -> None: parser.add_argument( '-t', '--hook-type', choices=( 'pre-commit', 'pre-merge-commit', 'pre-push', - 'prepare-commit-msg', 'commit-msg', 'post-checkout', + 'prepare-commit-msg', 'commit-msg', 'post-commit', 'post-checkout', ), action=AppendReplaceDefault, default=['pre-commit'], diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index ddf65b776..cce4a2584 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -96,6 +96,7 @@ def call(*_, **__): ('pre-merge-commit', []), ('pre-push', ['branch_name', 'remote_name']), ('commit-msg', ['.git/COMMIT_EDITMSG']), + ('post-commit', []), ('post-checkout', ['old_head', 'new_head', '1']), # multiple choices for commit-editmsg ('prepare-commit-msg', ['.git/COMMIT_EDITMSG']), @@ -149,6 +150,13 @@ def test_run_ns_commit_msg(): assert ns.commit_msg_filename == '.git/COMMIT_MSG' +def test_run_ns_post_commit(): + ns = hook_impl._run_ns('post-commit', True, (), b'') + assert ns is not None + assert ns.hook_stage == 'post-commit' + assert ns.color is True + + def test_run_ns_post_checkout(): ns = hook_impl._run_ns('post-checkout', True, ('a', 'b', 'c'), b'') assert ns is not None diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 66b91903b..6d75e68a5 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -726,6 +726,32 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): assert second_line.startswith('Must have "Signed off by:"...') +def test_post_commit_integration(tempdir_factory, store): + path = git_dir(tempdir_factory) + config = [ + { + 'repo': 'local', + 'hooks': [{ + 'id': 'post-commit', + 'name': 'Post commit', + 'entry': 'touch post-commit.tmp', + 'language': 'system', + 'always_run': True, + 'verbose': True, + 'stages': ['post-commit'], + }], + }, + ] + write_config(path, config) + with cwd(path): + _get_commit_output(tempdir_factory) + assert not os.path.exists('post-commit.tmp') + + install(C.CONFIG_FILE, store, hook_types=['post-commit']) + _get_commit_output(tempdir_factory) + assert os.path.exists('post-commit.tmp') + + def test_post_checkout_integration(tempdir_factory, store): path = git_dir(tempdir_factory) config = [ diff --git a/tests/repository_test.py b/tests/repository_test.py index 3c7a63727..f55c34c82 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -880,7 +880,7 @@ def test_manifest_hooks(tempdir_factory, store): require_serial=False, stages=( 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', - 'manual', 'post-checkout', 'push', + 'post-commit', 'manual', 'post-checkout', 'push', ), types=['file'], verbose=False, From e492a5578cb1bc6b0200e2b99f58eebb76884a16 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 29 Apr 2020 08:20:14 -0700 Subject: [PATCH 369/967] disable pip version check in python hooks --- pre_commit/languages/python.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 85d828107..de3dd4527 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -34,6 +34,7 @@ def bin_dir(venv: str) -> str: def get_env_patch(venv: str) -> PatchesT: return ( + ('PIP_DISABLE_PIP_VERSION_CHECK', '1'), ('PYTHONHOME', UNSET), ('VIRTUAL_ENV', venv), ('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))), From e2ed73209a64dba018af05b325e671d4b310416a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 2 May 2020 20:10:19 +0300 Subject: [PATCH 370/967] Add dummy go.mod for local "empty" installs --- pre_commit/resources/empty_template_go.mod | 0 pre_commit/store.py | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 pre_commit/resources/empty_template_go.mod diff --git a/pre_commit/resources/empty_template_go.mod b/pre_commit/resources/empty_template_go.mod new file mode 100644 index 000000000..e69de29bb diff --git a/pre_commit/store.py b/pre_commit/store.py index 760b37aaf..8bcbf99f7 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -182,9 +182,9 @@ def _git_cmd(*args: str) -> None: return self._new_repo(repo, ref, deps, clone_strategy) LOCAL_RESOURCES = ( - 'Cargo.toml', 'main.go', 'main.rs', '.npmignore', 'package.json', - 'pre_commit_dummy_package.gemspec', 'setup.py', 'environment.yml', - 'Makefile.PL', + 'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore', + 'package.json', 'pre_commit_dummy_package.gemspec', 'setup.py', + 'environment.yml', 'Makefile.PL', ) def make_local(self, deps: Sequence[str]) -> str: From 3b728fdb761b3a0f0d5e36ebf99d7c5ea8af26bf Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 2 May 2020 11:37:31 -0700 Subject: [PATCH 371/967] yay french strings --- pre_commit/languages/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 9b636d30d..26f4919e5 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -79,7 +79,7 @@ def install_environment( # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx?f=255&MSPPError=-2147217396#maxpath if sys.platform == 'win32': # pragma: no cover - envdir = f'\\\\?\\{os.path.normpath(envdir)}' + envdir = fr'\\?\{os.path.normpath(envdir)}' with clean_path_on_failure(envdir): cmd = [ sys.executable, '-mnodeenv', '--prebuilt', '--clean-src', envdir, From 928938a6a1aab3870da69471fe676e36fb4f42f9 Mon Sep 17 00:00:00 2001 From: Dom Date: Fri, 20 Mar 2020 15:48:44 +0000 Subject: [PATCH 372/967] fix hooks firing during staged_files_only --- pre_commit/commands/run.py | 6 +++++ pre_commit/staged_files_only.py | 9 ++++--- testing/util.py | 6 +++-- tests/commands/install_uninstall_test.py | 31 ++++++++++++++++++++++++ tests/commands/run_test.py | 6 +++++ 5 files changed, 53 insertions(+), 5 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 8a9352d4a..c2dab6f75 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -324,6 +324,12 @@ def run( f'`--hook-stage {args.hook_stage}`', ) return 1 + # prevent recursive post-checkout hooks (#1418) + if ( + args.hook_stage == 'post-checkout' and + environ.get('_PRE_COMMIT_SKIP_POST_CHECKOUT') + ): + return 0 # Expose from-ref / to-ref as environment variables for hooks to consume if args.from_ref and args.to_ref: diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 09d323dc7..617930102 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -56,8 +56,10 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: with open(patch_filename, 'wb') as patch_file: patch_file.write(diff_stdout_binary) - # Clear the working directory of unstaged changes - cmd_output_b('git', 'checkout', '--', '.') + # prevent recursive post-checkout hooks (#1418) + no_checkout_env = dict(os.environ, _PRE_COMMIT_SKIP_POST_CHECKOUT='1') + cmd_output_b('git', 'checkout', '--', '.', env=no_checkout_env) + try: yield finally: @@ -72,8 +74,9 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: # We failed to apply the patch, presumably due to fixes made # by hooks. # Roll back the changes made by hooks. - cmd_output_b('git', 'checkout', '--', '.') + cmd_output_b('git', 'checkout', '--', '.', env=no_checkout_env) _git_apply(patch_filename) + logger.info(f'Restored changes from {patch_filename}.') else: # There weren't any staged files so we don't need to do anything diff --git a/testing/util.py b/testing/util.py index 439bee794..19500f6fe 100644 --- a/testing/util.py +++ b/testing/util.py @@ -103,10 +103,12 @@ def cwd(path): os.chdir(original_cwd) -def git_commit(*args, fn=cmd_output, msg='commit!', **kwargs): +def git_commit(*args, fn=cmd_output, msg='commit!', all_files=True, **kwargs): kwargs.setdefault('stderr', subprocess.STDOUT) - cmd = ('git', 'commit', '--allow-empty', '--no-gpg-sign', '-a') + args + cmd = ('git', 'commit', '--allow-empty', '--no-gpg-sign', *args) + if all_files: # allow skipping `-a` with `all_files=False` + cmd += ('-a',) if msg is not None: # allow skipping `-m` with `msg=None` cmd += ('-m', msg) ret, out, _ = fn(*cmd, **kwargs) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 6d75e68a5..5809a3f27 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -789,6 +789,37 @@ def test_post_checkout_integration(tempdir_factory, store): assert 'some_file' not in stderr +def test_skips_post_checkout_unstaged_changes(tempdir_factory, store): + path = git_dir(tempdir_factory) + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'fail', + 'name': 'fail', + 'entry': 'fail', + 'language': 'fail', + 'always_run': True, + 'stages': ['post-checkout'], + }], + } + write_config(path, config) + with cwd(path): + cmd_output('git', 'add', '.') + _get_commit_output(tempdir_factory) + + install(C.CONFIG_FILE, store, hook_types=['pre-commit']) + install(C.CONFIG_FILE, store, hook_types=['post-checkout']) + + # make an unstaged change so staged_files_only fires + open('file', 'a').close() + cmd_output('git', 'add', 'file') + with open('file', 'w') as f: + f.write('unstaged changes') + + retc, out = _get_commit_output(tempdir_factory, all_files=False) + assert retc == 0 + + def test_prepare_commit_msg_integration_failing( failing_prepare_commit_msg_repo, tempdir_factory, store, ): diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index c51bcff0a..2fffdb911 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -1022,3 +1022,9 @@ def test_args_hook_only(cap_out, store, repo_with_passing_hook): run_opts(hook='do_not_commit'), ) assert b'identity-copy' not in printed + + +def test_skipped_without_any_setup_for_post_checkout(in_git_dir, store): + environ = {'_PRE_COMMIT_SKIP_POST_CHECKOUT': '1'} + opts = run_opts(hook_stage='post-checkout') + assert run(C.CONFIG_FILE, store, opts, environ=environ) == 0 From 3d50b3736a1349a70ab6c5dbf55acd5ccd07b526 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 2 May 2020 16:18:28 -0700 Subject: [PATCH 373/967] Improve python healthy() and eliminate python_venv - the `healthy()` check now requires virtualenv 20.x's metadata - `python_venv` is obsolete now that `virtualenv` generates the same structure and `virtualenv` is more portable --- pre_commit/languages/all.py | 4 +- pre_commit/languages/python.py | 141 +++++++++++++++------------- pre_commit/languages/python_venv.py | 46 --------- setup.cfg | 2 +- testing/gen-languages-all | 3 +- testing/util.py | 14 --- tests/languages/python_test.py | 96 ++++++++++++++++--- tests/repository_test.py | 2 - 8 files changed, 163 insertions(+), 145 deletions(-) delete mode 100644 pre_commit/languages/python_venv.py diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 8f4ffa8c5..5609631b0 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -14,7 +14,6 @@ from pre_commit.languages import perl from pre_commit.languages import pygrep from pre_commit.languages import python -from pre_commit.languages import python_venv from pre_commit.languages import ruby from pre_commit.languages import rust from pre_commit.languages import script @@ -49,7 +48,6 @@ class Language(NamedTuple): 'perl': Language(name='perl', ENVIRONMENT_DIR=perl.ENVIRONMENT_DIR, get_default_version=perl.get_default_version, healthy=perl.healthy, install_environment=perl.install_environment, run_hook=perl.run_hook), # noqa: E501 'pygrep': Language(name='pygrep', ENVIRONMENT_DIR=pygrep.ENVIRONMENT_DIR, get_default_version=pygrep.get_default_version, healthy=pygrep.healthy, install_environment=pygrep.install_environment, run_hook=pygrep.run_hook), # noqa: E501 'python': Language(name='python', ENVIRONMENT_DIR=python.ENVIRONMENT_DIR, get_default_version=python.get_default_version, healthy=python.healthy, install_environment=python.install_environment, run_hook=python.run_hook), # noqa: E501 - 'python_venv': Language(name='python_venv', ENVIRONMENT_DIR=python_venv.ENVIRONMENT_DIR, get_default_version=python_venv.get_default_version, healthy=python_venv.healthy, install_environment=python_venv.install_environment, run_hook=python_venv.run_hook), # noqa: E501 'ruby': Language(name='ruby', ENVIRONMENT_DIR=ruby.ENVIRONMENT_DIR, get_default_version=ruby.get_default_version, healthy=ruby.healthy, install_environment=ruby.install_environment, run_hook=ruby.run_hook), # noqa: E501 'rust': Language(name='rust', ENVIRONMENT_DIR=rust.ENVIRONMENT_DIR, get_default_version=rust.get_default_version, healthy=rust.healthy, install_environment=rust.install_environment, run_hook=rust.run_hook), # noqa: E501 'script': Language(name='script', ENVIRONMENT_DIR=script.ENVIRONMENT_DIR, get_default_version=script.get_default_version, healthy=script.healthy, install_environment=script.install_environment, run_hook=script.run_hook), # noqa: E501 @@ -57,4 +55,6 @@ class Language(NamedTuple): 'system': Language(name='system', ENVIRONMENT_DIR=system.ENVIRONMENT_DIR, get_default_version=system.get_default_version, healthy=system.healthy, install_environment=system.install_environment, run_hook=system.run_hook), # noqa: E501 # END GENERATED } +# TODO: fully deprecate `python_venv` +languages['python_venv'] = languages['python'] all_languages = sorted(languages) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index de3dd4527..e17376e1f 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -2,8 +2,7 @@ import functools import os import sys -from typing import Callable -from typing import ContextManager +from typing import Dict from typing import Generator from typing import Optional from typing import Sequence @@ -26,6 +25,28 @@ ENVIRONMENT_DIR = 'py_env' +@functools.lru_cache(maxsize=None) +def _version_info(exe: str) -> str: + prog = 'import sys;print(".".join(str(p) for p in sys.version_info))' + try: + return cmd_output(exe, '-S', '-c', prog)[1].strip() + except CalledProcessError: + return f'<>' + + +def _read_pyvenv_cfg(filename: str) -> Dict[str, str]: + ret = {} + with open(filename) as f: + for line in f: + try: + k, v = line.split('=') + except ValueError: # blank line / comment / etc. + continue + else: + ret[k.strip()] = v.strip() + return ret + + def bin_dir(venv: str) -> str: """On windows there's a different directory for the virtualenv""" bin_part = 'Scripts' if os.name == 'nt' else 'bin' @@ -116,6 +137,9 @@ def _sys_executable_matches(version: str) -> bool: def norm_version(version: str) -> str: + if version == C.DEFAULT: + return os.path.realpath(sys.executable) + # first see if our current executable is appropriate if _sys_executable_matches(version): return sys.executable @@ -140,70 +164,59 @@ def norm_version(version: str) -> str: return os.path.expanduser(version) -def py_interface( - _dir: str, - _make_venv: Callable[[str, str], None], -) -> Tuple[ - Callable[[Prefix, str], ContextManager[None]], - Callable[[Prefix, str], bool], - Callable[[Hook, Sequence[str], bool], Tuple[int, bytes]], - Callable[[Prefix, str, Sequence[str]], None], -]: - @contextlib.contextmanager - def in_env( - prefix: Prefix, - language_version: str, - ) -> Generator[None, None, None]: - envdir = prefix.path(helpers.environment_dir(_dir, language_version)) - with envcontext(get_env_patch(envdir)): - yield - - def healthy(prefix: Prefix, language_version: str) -> bool: - envdir = helpers.environment_dir(_dir, language_version) - exe_name = 'python.exe' if sys.platform == 'win32' else 'python' - py_exe = prefix.path(bin_dir(envdir), exe_name) - with in_env(prefix, language_version): - retcode, _, _ = cmd_output_b( - py_exe, '-c', 'import ctypes, datetime, io, os, ssl, weakref', - cwd='/', - retcode=None, - ) - return retcode == 0 - - def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, - ) -> Tuple[int, bytes]: - with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) - - def install_environment( - prefix: Prefix, - version: str, - additional_dependencies: Sequence[str], - ) -> None: - directory = helpers.environment_dir(_dir, version) - install = ('python', '-mpip', 'install', '.', *additional_dependencies) - - env_dir = prefix.path(directory) - with clean_path_on_failure(env_dir): - if version != C.DEFAULT: - python = norm_version(version) - else: - python = os.path.realpath(sys.executable) - _make_venv(env_dir, python) - with in_env(prefix, version): - helpers.run_setup_cmd(prefix, install) +@contextlib.contextmanager +def in_env( + prefix: Prefix, + language_version: str, +) -> Generator[None, None, None]: + directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version) + envdir = prefix.path(directory) + with envcontext(get_env_patch(envdir)): + yield - return in_env, healthy, run_hook, install_environment +def healthy(prefix: Prefix, language_version: str) -> bool: + directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version) + envdir = prefix.path(directory) + pyvenv_cfg = os.path.join(envdir, 'pyvenv.cfg') -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='/') + # created with "old" virtualenv + if not os.path.exists(pyvenv_cfg): + return False + + exe_name = 'python.exe' if sys.platform == 'win32' else 'python' + py_exe = prefix.path(bin_dir(envdir), exe_name) + cfg = _read_pyvenv_cfg(pyvenv_cfg) + + return ( + 'version_info' in cfg and + _version_info(py_exe) == cfg['version_info'] and ( + 'base-executable' not in cfg or + _version_info(cfg['base-executable']) == cfg['version_info'] + ) + ) -_interface = py_interface(ENVIRONMENT_DIR, make_venv) -in_env, healthy, run_hook, install_environment = _interface +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: + envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) + python = norm_version(version) + venv_cmd = (sys.executable, '-mvirtualenv', envdir, '-p', python) + install_cmd = ('python', '-mpip', 'install', '.', *additional_dependencies) + + with clean_path_on_failure(envdir): + cmd_output_b(*venv_cmd, cwd='/') + with in_env(prefix, version): + helpers.run_setup_cmd(prefix, install_cmd) + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: + with in_env(hook.prefix, hook.language_version): + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/python_venv.py b/pre_commit/languages/python_venv.py deleted file mode 100644 index 5404c8be5..000000000 --- a/pre_commit/languages/python_venv.py +++ /dev/null @@ -1,46 +0,0 @@ -import os.path - -from pre_commit.languages import python -from pre_commit.util import CalledProcessError -from pre_commit.util import cmd_output -from pre_commit.util import cmd_output_b - -ENVIRONMENT_DIR = 'py_venv' -get_default_version = python.get_default_version - - -def orig_py_exe(exe: str) -> str: # pragma: no cover (platform specific) - """A -mvenv virtualenv made from a -mvirtualenv virtualenv installs - packages to the incorrect location. Attempt to find the _original_ exe - and invoke `-mvenv` from there. - - See: - - https://github.com/pre-commit/pre-commit/issues/755 - - https://github.com/pypa/virtualenv/issues/1095 - - https://bugs.python.org/issue30811 - """ - try: - prefix_script = 'import sys; print(sys.real_prefix)' - _, prefix, _ = cmd_output(exe, '-c', prefix_script) - prefix = prefix.strip() - except CalledProcessError: - # not created from -mvirtualenv - return exe - - if os.name == 'nt': - expected = os.path.join(prefix, 'python.exe') - else: - expected = os.path.join(prefix, 'bin', os.path.basename(exe)) - - if os.path.exists(expected): - return expected - else: - return exe - - -def make_venv(envdir: str, python: str) -> None: - cmd_output_b(orig_py_exe(python), '-mvenv', envdir, cwd='/') - - -_interface = python.py_interface(ENVIRONMENT_DIR, make_venv) -in_env, healthy, run_hook, install_environment = _interface diff --git a/setup.cfg b/setup.cfg index 2e69d5032..2ca5b3150 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,7 +27,7 @@ install_requires = nodeenv>=0.11.1 pyyaml>=5.1 toml - virtualenv>=15.2 + virtualenv>=20.0.8 importlib-metadata;python_version<"3.8" importlib-resources;python_version<"3.7" python_requires = >=3.6.1 diff --git a/testing/gen-languages-all b/testing/gen-languages-all index 6d0b26ff9..2bff7beb0 100755 --- a/testing/gen-languages-all +++ b/testing/gen-languages-all @@ -3,8 +3,7 @@ import sys LANGUAGES = [ 'conda', 'docker', 'docker_image', 'fail', 'golang', 'node', 'perl', - 'pygrep', 'python', 'python_venv', 'ruby', 'rust', 'script', 'swift', - 'system', + 'pygrep', 'python', 'ruby', 'rust', 'script', 'swift', 'system', ] FIELDS = [ 'ENVIRONMENT_DIR', 'get_default_version', 'healthy', 'install_environment', diff --git a/testing/util.py b/testing/util.py index 439bee794..ff3537a47 100644 --- a/testing/util.py +++ b/testing/util.py @@ -45,20 +45,6 @@ def cmd_output_mocked_pre_commit_home( xfailif_windows = pytest.mark.xfail(os.name == 'nt', reason='windows') -def supports_venv(): # pragma: no cover (platform specific) - try: - __import__('ensurepip') - __import__('venv') - return True - except ImportError: - return False - - -xfailif_no_venv = pytest.mark.xfail( - not supports_venv(), reason='Does not support venv module', -) - - def run_opts( all_files=False, files=(), diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 34c6c7fc5..c419ad621 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -5,10 +5,23 @@ import pytest import pre_commit.constants as C +from pre_commit.envcontext import envcontext from pre_commit.languages import python from pre_commit.prefix import Prefix +def test_read_pyvenv_cfg(tmpdir): + pyvenv_cfg = tmpdir.join('pyvenv.cfg') + pyvenv_cfg.write( + '# I am a comment\n' + '\n' + 'foo = bar\n' + 'version-info=123\n', + ) + expected = {'foo': 'bar', 'version-info': '123'} + assert python._read_pyvenv_cfg(pyvenv_cfg) == expected + + def test_norm_version_expanduser(): home = os.path.expanduser('~') if os.name == 'nt': # pragma: nt cover @@ -21,6 +34,10 @@ def test_norm_version_expanduser(): assert result == expected_path +def test_norm_version_of_default_is_sys_executable(): + assert python.norm_version('default') == os.path.realpath(sys.executable) + + @pytest.mark.parametrize('v', ('python3.6', 'python3', 'python')) def test_sys_executable_matches(v): with mock.patch.object(sys, 'version_info', (3, 6, 7)): @@ -49,27 +66,78 @@ def test_find_by_sys_executable(exe, realpath, expected): assert python._find_by_sys_executable() == expected -def test_healthy_types_py_in_cwd(tmpdir): +@pytest.fixture +def python_dir(tmpdir): with tmpdir.as_cwd(): prefix = tmpdir.join('prefix').ensure_dir() prefix.join('setup.py').write('import setuptools; setuptools.setup()') prefix = Prefix(str(prefix)) - python.install_environment(prefix, C.DEFAULT, ()) + yield prefix, tmpdir - # even if a `types.py` file exists, should still be healthy - tmpdir.join('types.py').ensure() - assert python.healthy(prefix, C.DEFAULT) is True +def test_healthy_default_creator(python_dir): + prefix, tmpdir = python_dir -def test_healthy_python_goes_missing(tmpdir): - with tmpdir.as_cwd(): - prefix = tmpdir.join('prefix').ensure_dir() - prefix.join('setup.py').write('import setuptools; setuptools.setup()') - prefix = Prefix(str(prefix)) + python.install_environment(prefix, C.DEFAULT, ()) + + # should be healthy right after creation + assert python.healthy(prefix, C.DEFAULT) is True + + # even if a `types.py` file exists, should still be healthy + tmpdir.join('types.py').ensure() + assert python.healthy(prefix, C.DEFAULT) is True + + +def test_healthy_venv_creator(python_dir): + # venv creator produces slightly different pyvenv.cfg + prefix, tmpdir = python_dir + + with envcontext((('VIRTUALENV_CREATOR', 'venv'),)): python.install_environment(prefix, C.DEFAULT, ()) - exe_name = 'python' if sys.platform != 'win32' else 'python.exe' - py_exe = prefix.path(python.bin_dir('py_env-default'), exe_name) - os.remove(py_exe) + assert python.healthy(prefix, C.DEFAULT) is True + + +def test_unhealthy_python_goes_missing(python_dir): + prefix, tmpdir = python_dir + + python.install_environment(prefix, C.DEFAULT, ()) + + exe_name = 'python' if sys.platform != 'win32' else 'python.exe' + py_exe = prefix.path(python.bin_dir('py_env-default'), exe_name) + os.remove(py_exe) + + assert python.healthy(prefix, C.DEFAULT) is False + + +def test_unhealthy_with_version_change(python_dir): + prefix, tmpdir = python_dir + + python.install_environment(prefix, C.DEFAULT, ()) + + with open(prefix.path('py_env-default/pyvenv.cfg'), 'w') as f: + f.write('version_info = 1.2.3\n') + + assert python.healthy(prefix, C.DEFAULT) is False + + +def test_unhealthy_system_version_changes(python_dir): + prefix, tmpdir = python_dir + + python.install_environment(prefix, C.DEFAULT, ()) + + with open(prefix.path('py_env-default/pyvenv.cfg'), 'a') as f: + f.write('base-executable = /does/not/exist\n') + + assert python.healthy(prefix, C.DEFAULT) is False + + +def test_unhealthy_old_virtualenv(python_dir): + prefix, tmpdir = python_dir + + python.install_environment(prefix, C.DEFAULT, ()) + + # simulate "old" virtualenv by deleting this file + os.remove(prefix.path('py_env-default/pyvenv.cfg')) - assert python.healthy(prefix, C.DEFAULT) is False + assert python.healthy(prefix, C.DEFAULT) is False diff --git a/tests/repository_test.py b/tests/repository_test.py index f55c34c82..56e2bba8e 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -33,7 +33,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_no_venv from testing.util import xfailif_windows_no_ruby @@ -163,7 +162,6 @@ def test_python_hook_weird_setup_cfg(in_git_dir, tempdir_factory, store): ) -@xfailif_no_venv def test_python_venv(tempdir_factory, store): # pragma: no cover (no venv) _test_hook_repo( tempdir_factory, store, 'python_venv_hooks_repo', From c2375f2fa888c97888cc3719e76e2bb817cf5f82 Mon Sep 17 00:00:00 2001 From: Shunta Komatsu Date: Mon, 4 May 2020 14:16:53 +0900 Subject: [PATCH 374/967] Fix typo --- tests/commands/autoupdate_test.py | 12 ++++++------ tests/commands/hook_impl_test.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 25161d18c..fbeee7288 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -414,9 +414,9 @@ def test_autoupdate_local_hooks(in_git_dir, store): config = sample_local_config() add_config_to_repo('.', config) assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0 - new_config_writen = read_config('.') - assert len(new_config_writen['repos']) == 1 - assert new_config_writen['repos'][0] == config + new_config_written = read_config('.') + assert len(new_config_written['repos']) == 1 + assert new_config_written['repos'][0] == config def test_autoupdate_local_hooks_with_out_of_date_repo( @@ -429,9 +429,9 @@ def test_autoupdate_local_hooks_with_out_of_date_repo( config = {'repos': [local_config, stale_config]} write_config('.', config) assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0 - new_config_writen = read_config('.') - assert len(new_config_writen['repos']) == 2 - assert new_config_writen['repos'][0] == local_config + new_config_written = read_config('.') + assert len(new_config_written['repos']) == 2 + assert new_config_written['repos'][0] == local_config def test_autoupdate_meta_hooks(tmpdir, store): diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index cce4a2584..2fc014686 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -118,7 +118,7 @@ def test_check_args_length_error_too_many_plural(): ) -def test_check_args_length_error_too_many_singluar(): +def test_check_args_length_error_too_many_singular(): with pytest.raises(SystemExit) as excinfo: hook_impl._check_args_length('commit-msg', []) msg, = excinfo.value.args From 98d8a3d60fc3b52b8f2211f1979efb737ffb73af Mon Sep 17 00:00:00 2001 From: Marc Jay Date: Tue, 5 May 2020 00:42:54 +0100 Subject: [PATCH 375/967] Maintain scalar quoting style when autoupdate re-writes rev If rev is wrapped in single or double quotes (e.g. due to a yamllint quoted-strings rule), when re-writing the rev to update it, honour the existing quotation style --- pre_commit/commands/autoupdate.py | 12 +++++++----- pre_commit/util.py | 3 ++- tests/commands/autoupdate_test.py | 20 ++++++++++++++++++++ 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 8c9fdd7d0..87f6d53d2 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -84,7 +84,9 @@ def _check_hooks_still_exist_at_rev( ) -REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([^\s#]+)(.*)(\r?\n)$', re.DOTALL) +REV_LINE_RE = re.compile( + r'^(\s+)rev:(\s*)([\'"]?)([^\s#]+)(.*)(\r?\n)$', re.DOTALL, +) def _original_lines( @@ -116,15 +118,15 @@ def _write_new_config(path: str, rev_infos: List[Optional[RevInfo]]) -> None: continue match = REV_LINE_RE.match(lines[idx]) assert match is not None - new_rev_s = yaml_dump({'rev': rev_info.rev}) + new_rev_s = yaml_dump({'rev': rev_info.rev}, default_style=match[3]) new_rev = new_rev_s.split(':', 1)[1].strip() if rev_info.frozen is not None: comment = f' # frozen: {rev_info.frozen}' - elif match[4].strip().startswith('# frozen:'): + elif match[5].strip().startswith('# frozen:'): comment = '' else: - comment = match[4] - lines[idx] = f'{match[1]}rev:{match[2]}{new_rev}{comment}{match[5]}' + comment = match[5] + lines[idx] = f'{match[1]}rev:{match[2]}{new_rev}{comment}{match[6]}' with open(path, 'w', newline='') as f: f.write(''.join(lines)) diff --git a/pre_commit/util.py b/pre_commit/util.py index 2db579a5f..0338b3737 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -36,10 +36,11 @@ Dumper = getattr(yaml, 'CSafeDumper', yaml.SafeDumper) -def yaml_dump(o: Any) -> str: +def yaml_dump(o: Any, **kwargs: Any) -> str: # when python/mypy#1484 is solved, this can be `functools.partial` return yaml.dump( o, Dumper=Dumper, default_flow_style=False, indent=4, sort_keys=False, + **kwargs, ) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index fbeee7288..bd89c1dba 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -474,3 +474,23 @@ def test_updates_old_format_to_new_format(tmpdir, capsys, store): ) out, _ = capsys.readouterr() assert out == 'Configuration has been migrated.\n' + + +def test_maintains_rev_quoting_style(tmpdir, out_of_date, store): + fmt = ( + 'repos:\n' + '- repo: {path}\n' + ' rev: "{rev}"\n' + ' hooks:\n' + ' - id: foo\n' + '- repo: {path}\n' + " rev: '{rev}'\n" + ' hooks:\n' + ' - id: foo\n' + ) + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write(fmt.format(path=out_of_date.path, rev=out_of_date.original_rev)) + + assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + expected = fmt.format(path=out_of_date.path, rev=out_of_date.head_rev) + assert cfg.read() == expected From 4c154c3019db67d4948175203c183f44eba753dd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 8 May 2020 13:38:35 -0700 Subject: [PATCH 376/967] Use the real path of the cache root --- pre_commit/store.py | 3 ++- tests/store_test.py | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index 8bcbf99f7..6d8c40a93 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -30,10 +30,11 @@ def _get_default_directory() -> str: `Store.get_default_directory` can be mocked in tests and `_get_default_directory` can be tested. """ - return os.environ.get('PRE_COMMIT_HOME') or os.path.join( + ret = os.environ.get('PRE_COMMIT_HOME') or os.path.join( os.environ.get('XDG_CACHE_HOME') or os.path.expanduser('~/.cache'), 'pre-commit', ) + return os.path.realpath(ret) class Store: diff --git a/tests/store_test.py b/tests/store_test.py index 586661619..6a4e900c9 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -25,7 +25,8 @@ def test_our_session_fixture_works(): def test_get_default_directory_defaults_to_home(): # Not we use the module level one which is not mocked ret = _get_default_directory() - assert ret == os.path.join(os.path.expanduser('~/.cache'), 'pre-commit') + expected = os.path.realpath(os.path.expanduser('~/.cache/pre-commit')) + assert ret == expected def test_adheres_to_xdg_specification(): @@ -33,7 +34,8 @@ def test_adheres_to_xdg_specification(): os.environ, {'XDG_CACHE_HOME': '/tmp/fakehome'}, ): ret = _get_default_directory() - assert ret == os.path.join('/tmp/fakehome', 'pre-commit') + expected = os.path.realpath('/tmp/fakehome/pre-commit') + assert ret == expected def test_uses_environment_variable_when_present(): @@ -41,7 +43,8 @@ def test_uses_environment_variable_when_present(): os.environ, {'PRE_COMMIT_HOME': '/tmp/pre_commit_home'}, ): ret = _get_default_directory() - assert ret == '/tmp/pre_commit_home' + expected = os.path.realpath('/tmp/pre_commit_home') + assert ret == expected def test_store_init(store): From 8db02bd55076a107b1324fe1b2ad0f200c7260fc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 8 May 2020 15:55:10 -0700 Subject: [PATCH 377/967] xfail these tests on windows (access violation in npm) --- tests/repository_test.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/repository_test.py b/tests/repository_test.py index 56e2bba8e..855265d53 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -33,6 +33,7 @@ from testing.util import get_resource_path from testing.util import skipif_cant_run_docker from testing.util import skipif_cant_run_swift +from testing.util import xfailif_windows from testing.util import xfailif_windows_no_ruby @@ -241,6 +242,7 @@ def test_run_a_node_hook(tempdir_factory, store): ) +@xfailif_windows # pragma: win32 no cover def test_run_a_node_hook_default_version(tempdir_factory, store): # make sure that this continues to work for platforms where node is not # installed at the system @@ -250,6 +252,7 @@ def test_run_a_node_hook_default_version(tempdir_factory, store): test_run_a_node_hook(tempdir_factory, store) +@xfailif_windows # pragma: win32 no cover def test_run_versioned_node_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'node_versioned_hooks_repo', From 2e47f2dc724e7a5f056a9e06302116ce1a341c01 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 10 May 2020 11:30:37 -0700 Subject: [PATCH 378/967] xfail this one too --- tests/repository_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/repository_test.py b/tests/repository_test.py index 855265d53..19f7c0b37 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -535,6 +535,7 @@ def test_additional_ruby_dependencies_installed(tempdir_factory, store): assert 'tins' in output +@xfailif_windows def test_additional_node_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'node_hooks_repo') config = make_config_from_repo(path) From d89486b0f0b94c5a3f72d208e1d47e12ea07f104 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 10 May 2020 12:06:27 -0700 Subject: [PATCH 379/967] oh right, needs a no-cover for xfail --- tests/repository_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/repository_test.py b/tests/repository_test.py index 19f7c0b37..2ac788634 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -535,7 +535,7 @@ def test_additional_ruby_dependencies_installed(tempdir_factory, store): assert 'tins' in output -@xfailif_windows +@xfailif_windows # pragma: win32 no cover def test_additional_node_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'node_hooks_repo') config = make_config_from_repo(path) From b44461da33c2bc159f5b8156afd0732d5e696c0b Mon Sep 17 00:00:00 2001 From: Thierry Deo <6230277+tdeo@users.noreply.github.com> Date: Fri, 8 May 2020 12:00:18 +0200 Subject: [PATCH 380/967] Unset GEM_PATH for ruby hooks --- pre_commit/languages/ruby.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 61241f855..fe524ec3a 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -9,6 +9,7 @@ import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var from pre_commit.hook import Hook from pre_commit.languages import helpers @@ -28,6 +29,7 @@ def get_env_patch( ) -> PatchesT: # pragma: win32 no cover patches: PatchesT = ( ('GEM_HOME', os.path.join(venv, 'gems')), + ('GEM_PATH', UNSET), ('RBENV_ROOT', venv), ('BUNDLE_IGNORE_CONFIG', '1'), ( From 9b8e3d082dc3a8f712984fe128174dee20959103 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 10 May 2020 18:02:37 -0700 Subject: [PATCH 381/967] refuse to migrate an invalid configuration --- pre_commit/commands/migrate_config.py | 4 ++++ tests/commands/migrate_config_test.py | 7 ++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index d83b8e9cf..d580ff178 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -2,6 +2,7 @@ import yaml +from pre_commit.clientlib import load_config from pre_commit.util import yaml_load @@ -43,6 +44,9 @@ def _migrate_sha_to_rev(contents: str) -> str: def migrate_config(config_file: str, quiet: bool = False) -> int: + # ensure that the configuration is a valid pre-commit configuration + load_config(config_file) + with open(config_file) as f: orig_contents = contents = f.read() diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index efc0d1cb4..6a049d5f6 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -1,6 +1,7 @@ import pytest import pre_commit.constants as C +from pre_commit.clientlib import InvalidConfigError from pre_commit.commands.migrate_config import _indent from pre_commit.commands.migrate_config import migrate_config @@ -147,10 +148,10 @@ def test_migrate_config_sha_to_rev(tmpdir): @pytest.mark.parametrize('contents', ('', '\n')) -def test_empty_configuration_file_user_error(tmpdir, contents): +def test_migrate_config_invalid_configuration(tmpdir, contents): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write(contents) - with tmpdir.as_cwd(): - assert not migrate_config(C.CONFIG_FILE) + with tmpdir.as_cwd(), pytest.raises(InvalidConfigError): + migrate_config(C.CONFIG_FILE) # even though the config is invalid, this should be a noop assert cfg.read() == contents From 9641434163267732a4a5394fed02d88cf90abf00 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 11 May 2020 12:31:10 -0700 Subject: [PATCH 382/967] v2.4.0 --- CHANGELOG.md | 38 ++++++++++++++++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b833190e..75abd063d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,41 @@ +2.4.0 - 2020-05-11 +================== + +### Features +- Add support for `post-commit` hooks + - #1415 PR by @ModischFabrications. + - #1411 issue by @ModischFabrications. +- Silence pip version warning in python installation error + - #1412 PR by @asottile. +- Improve python `healthy()` when upgrading operating systems. + - #1431 PR by @asottile. + - #1427 issue by @ahonnecke. +- `language: python_venv` is now an alias to `language: python` (and will be + removed in a future version). + - #1431 PR by @asottile. +- Speed up python `healthy()` check. + - #1431 PR by @asottile. +- `pre-commit autoupdate` now tries to maintain quoting style of `rev`. + - #1435 PR by @marcjay. + - #1434 issue by @marcjay. + +### Fixes +- Fix installation of go modules in `repo: local`. + - #1428 PR by @scop. +- Fix committing with unstaged files and a failing `post-checkout` hook. + - #1422 PR by @domodwyer. + - #1418 issue by @domodwyer. +- Fix installation of node hooks with system node installed on freebsd. + - #1443 PR by @asottile. + - #1440 issue by @jockej. +- Fix ruby hooks when `GEM_PATH` is set globally. + - #1442 PR by @tdeo. +- Improve error message when `pre-commit autoupdate` / + `pre-commit migrate-config` are run but the pre-commit configuration is not + valid yaml. + - #1448 PR by @asottile. + - #1447 issue by @rpdelaney. + 2.3.0 - 2020-04-22 ================== diff --git a/setup.cfg b/setup.cfg index 2ca5b3150..08aae2647 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.3.0 +version = 2.4.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 6f2e86921309420cc618fa29b6ad5e655beefd71 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 11 May 2020 14:15:20 -0700 Subject: [PATCH 383/967] Run pre-commit autoupdate Committed via https://github.com/asottile/all-repos --- .pre-commit-config.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b51417d1d..36d73c7ab 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,25 +12,25 @@ repos: - id: requirements-txt-fixer - id: double-quote-string-fixer - repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.9 + rev: 3.8.0 hooks: - id: flake8 additional_dependencies: [flake8-typing-imports==1.6.0] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.5.1 + rev: v1.5.2 hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.2.0 + rev: v2.4.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.1.0 + rev: v2.4.1 hooks: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/asottile/reorder_python_imports - rev: v2.1.0 + rev: v2.3.0 hooks: - id: reorder-python-imports args: [--py3-plus] @@ -40,7 +40,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.8.2 + rev: v1.9.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy From 9e0b4a9d4df676557d4356ff7180e655a7b3e1c5 Mon Sep 17 00:00:00 2001 From: Chad Larson Date: Sat, 23 May 2020 17:20:26 -0500 Subject: [PATCH 384/967] pre-commit env var exposed --- pre_commit/commands/run.py | 3 +++ tests/commands/run_test.py | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index c2dab6f75..f6916c268 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -347,6 +347,9 @@ def run( if args.checkout_type: environ['PRE_COMMIT_CHECKOUT_TYPE'] = args.checkout_type + # Set pre_commit flag + environ['PRE_COMMIT'] = '1' + with contextlib.ExitStack() as exit_stack: if stash: exit_stack.enter_context(staged_files_only(store.directory)) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 2fffdb911..cf9794ede 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -1028,3 +1028,12 @@ def test_skipped_without_any_setup_for_post_checkout(in_git_dir, store): environ = {'_PRE_COMMIT_SKIP_POST_CHECKOUT': '1'} opts = run_opts(hook_stage='post-checkout') assert run(C.CONFIG_FILE, store, opts, environ=environ) == 0 + + +def test_pre_commit_env_variable_set(cap_out, store, repo_with_passing_hook): + args = run_opts() + environ: EnvironT = {} + ret, printed = _do_run( + cap_out, store, repo_with_passing_hook, args, environ, + ) + assert environ['PRE_COMMIT'] == '1' From 254c42864b124dc18c7e4f1b5e317a3e10a4f602 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 26 May 2020 21:53:16 -0700 Subject: [PATCH 385/967] slightly speed up tests by avoiding pre-commit install Committed via https://github.com/asottile/all-repos --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index d9f9420c9..63a3aab81 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,6 @@ commands = coverage erase coverage run -m pytest {posargs:tests} coverage report - pre-commit install [testenv:pre-commit] skip_install = true From 0781dac78f87729fbbe0eb830561da57d2183f58 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 27 May 2020 13:14:29 -0700 Subject: [PATCH 386/967] avoid a UnicodeError on windows with non-charmap characters --- pre_commit/languages/python.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index e17376e1f..6f7c90055 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -67,9 +67,10 @@ def _find_by_py_launcher( ) -> Optional[str]: # pragma: no cover (windows only) if version.startswith('python'): num = version[len('python'):] + cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)') + env = dict(os.environ, PYTHONIOENCODING='UTF-8') try: - cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)') - return cmd_output(*cmd)[1].strip() + return cmd_output(*cmd, env=env)[1].strip() except CalledProcessError: pass return None From e12082804220424cf35d8e6b62c26e2692945be8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 31 May 2020 12:42:17 -0700 Subject: [PATCH 387/967] use the shuffle method of Random instead --- azure-pipelines.yml | 4 ++-- pre_commit/languages/helpers.py | 4 ++-- tests/languages/helpers_test.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 9b385b4c1..c21843e10 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -10,7 +10,7 @@ resources: type: github endpoint: github name: asottile/azure-pipeline-templates - ref: refs/tags/v1.0.0 + ref: refs/tags/v2.0.0 jobs: - template: job--pre-commit.yml@asottile @@ -40,7 +40,7 @@ jobs: displayName: install swift - template: job--python-tox.yml@asottile parameters: - toxenvs: [pypy3, py36, py37, py38] + toxenvs: [pypy3, py36, py37, py38, py39] os: linux pre_test: - task: UseRubyVersion@0 diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index b5c95e522..01c65ab69 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -18,7 +18,7 @@ if TYPE_CHECKING: from typing import NoReturn -FIXED_RANDOM_SEED = 1542676186 +FIXED_RANDOM_SEED = 1542676187 def run_setup_cmd(prefix: Prefix, cmd: Tuple[str, ...]) -> None: @@ -92,7 +92,7 @@ def _shuffled(seq: Sequence[str]) -> List[str]: fixed_random.seed(FIXED_RANDOM_SEED, version=1) seq = list(seq) - random.shuffle(seq, random=fixed_random.random) + fixed_random.shuffle(seq) return seq diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index c52e947be..fa493cc04 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -78,5 +78,5 @@ def test_target_concurrency_cpu_count_not_implemented(): def test_shuffled_is_deterministic(): seq = [str(i) for i in range(10)] - expected = ['3', '7', '8', '2', '4', '6', '5', '1', '0', '9'] + expected = ['4', '0', '5', '1', '8', '6', '2', '3', '7', '9'] assert helpers._shuffled(seq) == expected From 5fb721f7a7415e5ba614710c6538caf30f7aed1c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 8 Jun 2020 13:29:31 -0700 Subject: [PATCH 388/967] normalize slashes even earlier on windows for filenames --- pre_commit/commands/run.py | 27 ++++++++++++------- pre_commit/meta_hooks/check_hooks_apply.py | 7 +++-- .../meta_hooks/check_useless_excludes.py | 7 +++-- tests/commands/run_test.py | 4 +-- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index f6916c268..567b7cd3b 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -72,13 +72,7 @@ def filter_by_include_exclude( class Classifier: - def __init__(self, filenames: Sequence[str]) -> None: - # on windows we normalize all filenames to use forward slashes - # this makes it easier to filter using the `files:` regex - # this also makes improperly quoted shell-based hooks work better - # see #1173 - if os.altsep == '/' and os.sep == '\\': - filenames = [f.replace(os.sep, os.altsep) for f in filenames] + def __init__(self, filenames: Collection[str]) -> None: self.filenames = [f for f in filenames if os.path.lexists(f)] @functools.lru_cache(maxsize=None) @@ -105,6 +99,22 @@ def filenames_for_hook(self, hook: Hook) -> Tuple[str, ...]: names = self.by_types(names, hook.types, hook.exclude_types) return tuple(names) + @classmethod + def from_config( + cls, + filenames: Collection[str], + include: str, + exclude: str, + ) -> 'Classifier': + # on windows we normalize all filenames to use forward slashes + # this makes it easier to filter using the `files:` regex + # this also makes improperly quoted shell-based hooks work better + # see #1173 + if os.altsep == '/' and os.sep == '\\': + filenames = [f.replace(os.sep, os.altsep) for f in filenames] + filenames = filter_by_include_exclude(filenames, include, exclude) + return Classifier(filenames) + def _get_skips(environ: EnvironT) -> Set[str]: skips = environ.get('SKIP', '') @@ -247,10 +257,9 @@ def _run_hooks( """Actually run the hooks.""" skips = _get_skips(environ) cols = _compute_cols(hooks) - filenames = filter_by_include_exclude( + classifier = Classifier.from_config( _all_filenames(args), config['files'], config['exclude'], ) - classifier = Classifier(filenames) retval = 0 for hook in hooks: retval |= _run_single_hook( diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index d0244a944..a1e93529a 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -11,10 +11,13 @@ def check_all_hooks_match_files(config_file: str) -> int: - classifier = Classifier(git.get_all_files()) + config = load_config(config_file) + classifier = Classifier.from_config( + git.get_all_files(), config['files'], config['exclude'], + ) retv = 0 - for hook in all_hooks(load_config(config_file), Store()): + for hook in all_hooks(config, Store()): if hook.always_run or hook.language == 'fail': continue elif not classifier.filenames_for_hook(hook): diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 30b8d8101..db6865c6c 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -28,11 +28,14 @@ def exclude_matches_any( def check_useless_excludes(config_file: str) -> int: config = load_config(config_file) - classifier = Classifier(git.get_all_files()) + filenames = git.get_all_files() + classifier = Classifier.from_config( + filenames, config['files'], config['exclude'], + ) retv = 0 exclude = config['exclude'] - if not exclude_matches_any(classifier.filenames, '', exclude): + if not exclude_matches_any(filenames, '', exclude): print( f'The global exclude pattern {exclude!r} does not match any files', ) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index cf9794ede..2461ed5b3 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -939,7 +939,7 @@ def test_classifier_normalizes_filenames_on_windows_to_forward_slashes(tmpdir): tmpdir.join('a/b/c').ensure() with mock.patch.object(os, 'altsep', '/'): with mock.patch.object(os, 'sep', '\\'): - classifier = Classifier((r'a\b\c',)) + classifier = Classifier.from_config((r'a\b\c',), '', '^$') assert classifier.filenames == ['a/b/c'] @@ -947,7 +947,7 @@ def test_classifier_does_not_normalize_backslashes_non_windows(tmpdir): with mock.patch.object(os.path, 'lexists', return_value=True): with mock.patch.object(os, 'altsep', None): with mock.patch.object(os, 'sep', '/'): - classifier = Classifier((r'a/b\c',)) + classifier = Classifier.from_config((r'a/b\c',), '', '^$') assert classifier.filenames == [r'a/b\c'] From 2f25085d60bf953fea457a3240c91886145dbcc5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 8 Jun 2020 15:17:13 -0700 Subject: [PATCH 389/967] v2.5.0 --- CHANGELOG.md | 21 +++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75abd063d..55bbd3ce4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +2.5.0 - 2020-06-08 +================== + +### Features +- Expose a `PRE_COMMIT=1` environment variable when running hooks + - #1467 PR by @tech-chad. + - #1426 issue by @lorenzwalthert. + +### Fixes +- Fix `UnicodeDecodeError` on windows when using the `py` launcher to detect + executables with non-ascii characters in the path + - #1474 PR by @asottile. + - #1472 issue by DrFobos. +- Fix `DeprecationWarning` on python3.9 for `random.shuffle` method + - #1480 PR by @asottile. + - #1479 issue by @isidentical. +- Normalize slashes earlier such that global `files` / `exclude` use forward + slashes on windows as well. + - #1494 PR by @asottile. + - #1476 issue by @harrybiddle. + 2.4.0 - 2020-05-11 ================== diff --git a/setup.cfg b/setup.cfg index 08aae2647..03e31d1e4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.4.0 +version = 2.5.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 6ee9e13b2686f66c0e796947dfcb775201b1c3d6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 9 Jun 2020 09:45:40 -0700 Subject: [PATCH 390/967] prevent infinite recursion of post-checkout on clone --- pre_commit/git.py | 3 ++- tests/git_test.py | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 7e757f247..576bef8cc 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -158,7 +158,8 @@ def init_repo(path: str, remote: str) -> None: remote = os.path.abspath(remote) env = no_git_env() - cmd_output_b('git', 'init', path, env=env) + # avoid the user's template so that hooks do not recurse + cmd_output_b('git', 'init', '--template=', path, env=env) cmd_output_b('git', 'remote', 'add', 'origin', remote, cwd=path, env=env) diff --git a/tests/git_test.py b/tests/git_test.py index e73a6f240..fafd4a6e3 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -186,3 +186,8 @@ def test_no_git_env(): 'GIT_SSH': '/usr/bin/ssh', 'GIT_SSH_COMMAND': 'ssh -o', } + + +def test_init_repo_no_hooks(tmpdir): + git.init_repo(str(tmpdir), remote='dne') + assert not tmpdir.join('.git/hooks').exists() From 0e5eb199292d44107a591f8bf886874f18f0a304 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 9 Jun 2020 14:18:42 -0700 Subject: [PATCH 391/967] v2.5.1 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55bbd3ce4..375a9f3b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +2.5.1 - 2020-06-09 +================== + +### Fixes +- Prevent infinite recursion of post-checkout on clone + - #1497 PR by @asottile. + - #1496 issue by @admorgan. + 2.5.0 - 2020-06-08 ================== diff --git a/setup.cfg b/setup.cfg index 03e31d1e4..f1ce18d60 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.5.0 +version = 2.5.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From fd53cdea17ed17a1775fc5e23e75d6ecdbdb04b6 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Fri, 12 Jun 2020 13:26:29 +0200 Subject: [PATCH 392/967] Add foor missing required dependencies in CONTRIBUTING.md There could still be missing dependencies, I'm not using a fresh environement to test that. Also added the specific required git version see https://github.com/git/git/blob/master/Documentation/RelNotes/2.24.0.txt --- CONTRIBUTING.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d70a89dd9..76df43705 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,12 +4,16 @@ - The complete test suite depends on having at least the following installed (possibly not a complete list) - - git (A sufficiently newer version is required to run pre-push tests) + - git (Version 2.24.0 or above is required to run pre-merge-commit tests) - python2 (Required by a test which checks different python versions) - python3 (Required by a test which checks different python versions) - tox (or virtualenv) - ruby + gem - docker + - conda + - cargo (required by tests for rust dependencies) + - go (required by tests for go dependencies) + - swift ### Setting up an environment From e1e6a32c512275e4f7bf7a057bc5d4baa27303fa Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 15 Jun 2020 13:50:47 -0700 Subject: [PATCH 393/967] skip rbenv if ruby and gem are installed with default language_version --- azure-pipelines.yml | 1 + pre_commit/languages/ruby.py | 73 +++++++++++++++++++++++------------- testing/util.py | 4 -- tests/languages/ruby_test.py | 32 ++++++++++++++-- tests/repository_test.py | 7 +--- 5 files changed, 78 insertions(+), 39 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c21843e10..fb400107d 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -19,6 +19,7 @@ jobs: toxenvs: [py37] os: windows pre_test: + - task: UseRubyVersion@0 - powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts" displayName: Add conda to PATH - powershell: | diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index fe524ec3a..73b23cc07 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -1,4 +1,5 @@ import contextlib +import functools import os.path import shutil import tarfile @@ -7,6 +8,7 @@ from typing import Tuple import pre_commit.constants as C +from pre_commit import parse_shebang from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET @@ -19,33 +21,51 @@ from pre_commit.util import resource_bytesio ENVIRONMENT_DIR = 'rbenv' -get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy +@functools.lru_cache(maxsize=1) +def get_default_version() -> str: + if all(parse_shebang.find_executable(exe) for exe in ('ruby', 'gem')): + return 'system' + else: + return C.DEFAULT + + def get_env_patch( venv: str, language_version: str, -) -> PatchesT: # pragma: win32 no cover +) -> PatchesT: patches: PatchesT = ( ('GEM_HOME', os.path.join(venv, 'gems')), ('GEM_PATH', UNSET), - ('RBENV_ROOT', venv), ('BUNDLE_IGNORE_CONFIG', '1'), - ( - 'PATH', ( - os.path.join(venv, 'gems', 'bin'), os.pathsep, - os.path.join(venv, 'shims'), os.pathsep, - os.path.join(venv, 'bin'), os.pathsep, Var('PATH'), - ), - ), ) - if language_version != C.DEFAULT: - patches += (('RBENV_VERSION', language_version),) + if language_version == 'system': + patches += ( + ( + 'PATH', ( + os.path.join(venv, 'gems', 'bin'), os.pathsep, + Var('PATH'), + ), + ), + ) + else: # pragma: win32 no cover + patches += ( + ('RBENV_ROOT', venv), + ('RBENV_VERSION', language_version), + ( + 'PATH', ( + os.path.join(venv, 'gems', 'bin'), os.pathsep, + os.path.join(venv, 'shims'), os.pathsep, + os.path.join(venv, 'bin'), os.pathsep, Var('PATH'), + ), + ), + ) return patches -@contextlib.contextmanager # pragma: win32 no cover +@contextlib.contextmanager def in_env( prefix: Prefix, language_version: str, @@ -65,7 +85,7 @@ def _extract_resource(filename: str, dest: str) -> None: def _install_rbenv( prefix: Prefix, - version: str = C.DEFAULT, + version: str, ) -> None: # pragma: win32 no cover directory = helpers.environment_dir(ENVIRONMENT_DIR, version) @@ -92,21 +112,22 @@ def _install_ruby( def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], -) -> None: # pragma: win32 no cover +) -> None: additional_dependencies = tuple(additional_dependencies) directory = helpers.environment_dir(ENVIRONMENT_DIR, version) with clean_path_on_failure(prefix.path(directory)): - # TODO: this currently will fail if there's no version specified and - # there's no system ruby installed. Is this ok? - _install_rbenv(prefix, version=version) - with in_env(prefix, version): - # Need to call this before installing so rbenv's directories are - # set up - helpers.run_setup_cmd(prefix, ('rbenv', 'init', '-')) - if version != C.DEFAULT: + if version != 'system': # pragma: win32 no cover + _install_rbenv(prefix, version) + with in_env(prefix, version): + # Need to call this before installing so rbenv's directories + # are set up + helpers.run_setup_cmd(prefix, ('rbenv', 'init', '-')) + # XXX: this will *always* fail if `version == C.DEFAULT` _install_ruby(prefix, version) - # Need to call this after installing to set up the shims - helpers.run_setup_cmd(prefix, ('rbenv', 'rehash')) + # Need to call this after installing to set up the shims + helpers.run_setup_cmd(prefix, ('rbenv', 'rehash')) + + with in_env(prefix, version): helpers.run_setup_cmd( prefix, ('gem', 'build', *prefix.star('.gemspec')), ) @@ -123,6 +144,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: # pragma: win32 no cover +) -> Tuple[int, bytes]: with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/testing/util.py b/testing/util.py index bfe142188..4edb7a9ec 100644 --- a/testing/util.py +++ b/testing/util.py @@ -38,10 +38,6 @@ def cmd_output_mocked_pre_commit_home( parse_shebang.find_executable('swift') is None, reason="swift isn't installed or can't be found", ) -xfailif_windows_no_ruby = pytest.mark.xfail( - os.name == 'nt', - reason='Ruby support not yet implemented on windows.', -) xfailif_windows = pytest.mark.xfail(os.name == 'nt', reason='windows') diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 36a029d17..853bb7321 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -1,15 +1,39 @@ import os.path +from unittest import mock +import pytest + +import pre_commit.constants as C +from pre_commit import parse_shebang from pre_commit.languages import ruby from pre_commit.prefix import Prefix from pre_commit.util import cmd_output -from testing.util import xfailif_windows_no_ruby +from testing.util import xfailif_windows + + +ACTUAL_GET_DEFAULT_VERSION = ruby.get_default_version.__wrapped__ + + +@pytest.fixture +def find_exe_mck(): + with mock.patch.object(parse_shebang, 'find_executable') as mck: + yield mck + + +def test_uses_default_version_when_not_available(find_exe_mck): + find_exe_mck.return_value = None + assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT + + +def test_uses_system_if_both_gem_and_ruby_are_available(find_exe_mck): + find_exe_mck.return_value = '/path/to/exe' + assert ACTUAL_GET_DEFAULT_VERSION() == 'system' -@xfailif_windows_no_ruby +@xfailif_windows # pragma: win32 no cover def test_install_rbenv(tempdir_factory): prefix = Prefix(tempdir_factory.get()) - ruby._install_rbenv(prefix) + ruby._install_rbenv(prefix, C.DEFAULT) # Should have created rbenv directory assert os.path.exists(prefix.path('rbenv-default')) @@ -18,7 +42,7 @@ def test_install_rbenv(tempdir_factory): cmd_output('rbenv', '--help') -@xfailif_windows_no_ruby +@xfailif_windows # pragma: win32 no cover def test_install_rbenv_with_version(tempdir_factory): prefix = Prefix(tempdir_factory.get()) ruby._install_rbenv(prefix, version='1.9.3p547') diff --git a/tests/repository_test.py b/tests/repository_test.py index 2ac788634..d3b3dd550 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -34,7 +34,6 @@ from testing.util import skipif_cant_run_docker from testing.util import skipif_cant_run_swift from testing.util import xfailif_windows -from testing.util import xfailif_windows_no_ruby def _norm_out(b): @@ -260,7 +259,6 @@ def test_run_versioned_node_hook(tempdir_factory, store): ) -@xfailif_windows_no_ruby def test_run_a_ruby_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'ruby_hooks_repo', @@ -268,7 +266,7 @@ def test_run_a_ruby_hook(tempdir_factory, store): ) -@xfailif_windows_no_ruby +@xfailif_windows # pragma: win32 no cover def test_run_versioned_ruby_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'ruby_versioned_hooks_repo', @@ -278,7 +276,7 @@ def test_run_versioned_ruby_hook(tempdir_factory, store): ) -@xfailif_windows_no_ruby +@xfailif_windows # pragma: win32 no cover def test_run_ruby_hook_with_disable_shared_gems( tempdir_factory, store, @@ -524,7 +522,6 @@ def test_additional_dependencies_roll_forward(tempdir_factory, store): assert 'mccabe' not in cmd_output('pip', 'freeze', '-l')[1] -@xfailif_windows_no_ruby # pragma: win32 no cover def test_additional_ruby_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'ruby_hooks_repo') config = make_config_from_repo(path) From 1392471854f6a2bdf57bafd3b56c9da183fad331 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 17 Jun 2020 12:55:30 -0700 Subject: [PATCH 394/967] xfail a flaky node test on windows --- tests/repository_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/repository_test.py b/tests/repository_test.py index d3b3dd550..ee57d9924 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -234,6 +234,7 @@ def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): ) +@xfailif_windows # pragma: win32 no cover def test_run_a_node_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'node_hooks_repo', From 6ec47ea73640190d86186fd50b7a32fa0a9c4b9b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 19 Jun 2020 13:58:14 -0700 Subject: [PATCH 395/967] fix node hooks when NPM_CONFIG_USERCONFIG is set --- pre_commit/languages/node.py | 3 +++ tests/repository_test.py | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 26f4919e5..d99e6f2c7 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -10,6 +10,7 @@ from pre_commit import parse_shebang from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var from pre_commit.hook import Hook from pre_commit.languages import helpers @@ -56,6 +57,8 @@ def get_env_patch(venv: str) -> PatchesT: ('NODE_VIRTUAL_ENV', venv), ('NPM_CONFIG_PREFIX', install_prefix), ('npm_config_prefix', install_prefix), + ('NPM_CONFIG_USERCONFIG', UNSET), + ('npm_config_userconfig', UNSET), ('NODE_PATH', os.path.join(venv, lib_dir, 'node_modules')), ('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))), ) diff --git a/tests/repository_test.py b/tests/repository_test.py index ee57d9924..84e4da930 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -260,6 +260,14 @@ def test_run_versioned_node_hook(tempdir_factory, store): ) +@xfailif_windows # pragma: win32 no cover +def test_node_hook_with_npm_userconfig_set(tempdir_factory, store, tmpdir): + cfg = tmpdir.join('cfg') + cfg.write('cache=/dne\n') + with mock.patch.dict(os.environ, NPM_CONFIG_USERCONFIG=str(cfg)): + test_run_a_node_hook(tempdir_factory, store) + + def test_run_a_ruby_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'ruby_hooks_repo', From 6fe1702ee106f4d7f4a8ad73550db2145208ef24 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 1 Jul 2020 12:39:34 -0700 Subject: [PATCH 396/967] v2.6.0 --- CHANGELOG.md | 15 +++++++++++++++ setup.cfg | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 375a9f3b8..c487acf6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +2.6.0 - 2020-07-01 +================== + +### Fixes +- Fix node hooks when `NPM_CONFIG_USERCONFIG` is set + - #1521 PR by @asottile. + - #1516 issue by @rkm. + +### Features +- Skip `rbenv` / `ruby-download` if system ruby is available + - #1509 PR by @asottile. +- Partial support for ruby on windows (if system ruby is installed) + - #1509 PR by @asottile. + - #201 issue by @asottile. + 2.5.1 - 2020-06-09 ================== diff --git a/setup.cfg b/setup.cfg index f1ce18d60..0ce58b1a2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.5.1 +version = 2.6.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From c9ad2e14515537c1f7f3857804f69bb7fb05b3b1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 8 Jul 2020 13:55:28 -0700 Subject: [PATCH 397/967] upgrade mypy to get typeshed fixes --- .pre-commit-config.yaml | 14 +++++++------- pre_commit/file_lock.py | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 36d73c7ab..e9cf73946 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.5.0 + rev: v3.1.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -12,20 +12,20 @@ repos: - id: requirements-txt-fixer - id: double-quote-string-fixer - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.0 + rev: 3.8.3 hooks: - id: flake8 additional_dependencies: [flake8-typing-imports==1.6.0] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.5.2 + rev: v1.5.3 hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.4.0 + rev: v2.6.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.4.1 + rev: v2.6.2 hooks: - id: pyupgrade args: [--py36-plus] @@ -40,11 +40,11 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.9.0 + rev: v1.10.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.770 + rev: v0.782 hooks: - id: mypy exclude: ^testing/resources/ diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index ff0dc5e64..5e7a05862 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -21,13 +21,13 @@ def _locked( ) -> Generator[None, None, None]: try: # TODO: https://github.com/python/typeshed/pull/3607 - msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region) # type: ignore + msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region) except OSError: blocked_cb() while True: try: # TODO: https://github.com/python/typeshed/pull/3607 - msvcrt.locking(fileno, msvcrt.LK_LOCK, _region) # type: ignore # noqa: E501 + msvcrt.locking(fileno, msvcrt.LK_LOCK, _region) except OSError as e: # Locking violation. Returned when the _LK_LOCK or _LK_RLCK # flag is specified and the file cannot be locked after 10 @@ -46,7 +46,7 @@ def _locked( # "Regions should be locked only briefly and should be unlocked # before closing a file or exiting the program." # TODO: https://github.com/python/typeshed/pull/3607 - msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region) # type: ignore + msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region) else: # pragma: win32 no cover import fcntl From 7da72563dd3c9c0c73292c9a3ab5cc10061132f6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 15 Jul 2020 21:07:21 -0700 Subject: [PATCH 398/967] require healthy() after installation --- pre_commit/repository.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 77734ee64..91c430555 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -82,6 +82,12 @@ def _hook_install(hook: Hook) -> None: lang.install_environment( hook.prefix, hook.language_version, hook.additional_dependencies, ) + if not lang.healthy(hook.prefix, hook.language_version): + raise AssertionError( + f'BUG: expected environment for {hook.language} to be healthy() ' + f'immediately after install, please open an issue describing ' + f'your environment', + ) # Write our state to indicate we're installed _write_state(hook.prefix, venv, _state(hook.additional_dependencies)) From 1b435f1f1fa7432cbb1b2bef61c3ec0071d036cb Mon Sep 17 00:00:00 2001 From: Greg Singer Date: Sun, 19 Jul 2020 16:37:44 -0500 Subject: [PATCH 399/967] add init-templatedir --no-allow-missing-config Add a `--no-allow-missing-config` option to the `init-templatedir` command. Enable configuration of a Git template that requires newly cloned repos to have a `pre-commit` config. --- pre_commit/commands/init_templatedir.py | 9 +++-- pre_commit/main.py | 7 ++++ tests/commands/init_templatedir_test.py | 48 +++++++++++++++++++++++++ tests/main_test.py | 21 +++++++++++ 4 files changed, 83 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/init_templatedir.py b/pre_commit/commands/init_templatedir.py index f676fb192..5f17d9c12 100644 --- a/pre_commit/commands/init_templatedir.py +++ b/pre_commit/commands/init_templatedir.py @@ -15,10 +15,15 @@ def init_templatedir( store: Store, directory: str, hook_types: Sequence[str], + skip_on_missing_config: bool = True, ) -> int: install( - config_file, store, hook_types=hook_types, - overwrite=True, skip_on_missing_config=True, git_dir=directory, + config_file, + store, + hook_types=hook_types, + overwrite=True, + skip_on_missing_config=skip_on_missing_config, + git_dir=directory, ) try: _, out, _ = cmd_output('git', 'config', 'init.templateDir') diff --git a/pre_commit/main.py b/pre_commit/main.py index 874eb53a5..ffcc2e875 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -245,6 +245,12 @@ def main(argv: Optional[Sequence[str]] = None) -> int: init_templatedir_parser.add_argument( 'directory', help='The directory in which to write the hook script.', ) + init_templatedir_parser.add_argument( + '--no-allow-missing-config', + action='store_false', + dest='allow_missing_config', + help='Assume cloned repos should have a `pre-commit` config.', + ) _add_hook_type_option(init_templatedir_parser) install_parser = subparsers.add_parser( @@ -383,6 +389,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: return init_templatedir( args.config, store, args.directory, hook_types=args.hook_types, + skip_on_missing_config=args.allow_missing_config, ) elif args.command == 'install-hooks': return install_hooks(args.config, store) diff --git a/tests/commands/init_templatedir_test.py b/tests/commands/init_templatedir_test.py index d14a171f6..4e131dff7 100644 --- a/tests/commands/init_templatedir_test.py +++ b/tests/commands/init_templatedir_test.py @@ -1,6 +1,8 @@ import os.path from unittest import mock +import pytest + import pre_commit.constants as C from pre_commit.commands.init_templatedir import init_templatedir from pre_commit.envcontext import envcontext @@ -90,3 +92,49 @@ def test_init_templatedir_hookspath_set(tmpdir, tempdir_factory, store): C.CONFIG_FILE, store, target, hook_types=['pre-commit'], ) assert target.join('hooks/pre-commit').exists() + + +@pytest.mark.parametrize( + ('skip', 'commit_retcode', 'commit_output_snippet'), + ( + (True, 0, 'Skipping `pre-commit`.'), + (False, 1, f'No {C.CONFIG_FILE} file was found'), + ), +) +def test_init_templatedir_skip_on_missing_config( + tmpdir, + tempdir_factory, + store, + cap_out, + skip, + commit_retcode, + commit_output_snippet, +): + target = str(tmpdir.join('tmpl')) + init_git_dir = git_dir(tempdir_factory) + with cwd(init_git_dir): + cmd_output('git', 'config', 'init.templateDir', target) + init_templatedir( + C.CONFIG_FILE, + store, + target, + hook_types=['pre-commit'], + skip_on_missing_config=skip, + ) + + lines = cap_out.get().splitlines() + assert len(lines) == 1 + assert lines[0].startswith('pre-commit installed at') + + with envcontext((('GIT_TEMPLATE_DIR', target),)): + verify_git_dir = git_dir(tempdir_factory) + + with cwd(verify_git_dir): + retcode, output = git_commit( + fn=cmd_output_mocked_pre_commit_home, + tempdir_factory=tempdir_factory, + retcode=None, + ) + + assert retcode == commit_retcode + assert commit_output_snippet in output diff --git a/tests/main_test.py b/tests/main_test.py index c4724768c..f7abeeb4d 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -159,7 +159,28 @@ def test_try_repo(mock_store_dir): def test_init_templatedir(mock_store_dir): with mock.patch.object(main, 'init_templatedir') as patch: main.main(('init-templatedir', 'tdir')) + + assert patch.call_count == 1 + assert 'tdir' in patch.call_args[0] + assert patch.call_args[1]['hook_types'] == ['pre-commit'] + assert patch.call_args[1]['skip_on_missing_config'] is True + + +def test_init_templatedir_options(mock_store_dir): + args = ( + 'init-templatedir', + 'tdir', + '--hook-type', + 'commit-msg', + '--no-allow-missing-config', + ) + with mock.patch.object(main, 'init_templatedir') as patch: + main.main(args) + assert patch.call_count == 1 + assert 'tdir' in patch.call_args[0] + assert patch.call_args[1]['hook_types'] == ['commit-msg'] + assert patch.call_args[1]['skip_on_missing_config'] is False def test_help_cmd_in_empty_directory( From cee834bb5e643b80128e929dc9fb873e7d88c1e8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 24 Jul 2020 15:56:35 -0700 Subject: [PATCH 400/967] better error handling when Store is readonly --- pre_commit/error_handler.py | 13 ++++++++++--- tests/error_handler_test.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index b2321ae0d..13d78cbbd 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -18,10 +18,17 @@ class FatalError(RuntimeError): def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None: error_msg = f'{msg}: {type(exc).__name__}: '.encode() + force_bytes(exc) output.write_line_b(error_msg) - log_path = os.path.join(Store().directory, 'pre-commit.log') - output.write_line(f'Check the log at {log_path}') - with open(log_path, 'wb') as log: + storedir = Store().directory + log_path = os.path.join(storedir, 'pre-commit.log') + with contextlib.ExitStack() as ctx: + if os.access(storedir, os.W_OK): + output.write_line(f'Check the log at {log_path}') + log = ctx.enter_context(open(log_path, 'wb')) + else: # pragma: win32 no cover + output.write_line(f'Failed to write to log at {log_path}') + log = sys.stdout.buffer + _log_line = functools.partial(output.write_line, stream=log) _log_line_b = functools.partial(output.write_line_b, stream=log) diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 833bb8f83..d066e5728 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -1,13 +1,16 @@ import os.path import re +import stat import sys from unittest import mock import pytest from pre_commit import error_handler +from pre_commit.store import Store from pre_commit.util import CalledProcessError from testing.util import cmd_output_mocked_pre_commit_home +from testing.util import xfailif_windows @pytest.fixture @@ -168,3 +171,29 @@ def test_error_handler_no_tty(tempdir_factory): out_lines = out.splitlines() assert out_lines[-2] == 'An unexpected error has occurred: ValueError: ☃' assert out_lines[-1] == f'Check the log at {log_file}' + + +@xfailif_windows # pragma: win32 no cover +def test_error_handler_read_only_filesystem(mock_store_dir, cap_out, capsys): + # a better scenario would be if even the Store crash would be handled + # but realistically we're only targetting systems where the Store has + # already been set up + Store() + + write = (stat.S_IWGRP | stat.S_IWOTH | stat.S_IWUSR) + os.chmod(mock_store_dir, os.stat(mock_store_dir).st_mode & ~write) + + with pytest.raises(SystemExit): + with error_handler.error_handler(): + raise ValueError('ohai') + + output = cap_out.get() + assert output.startswith( + 'An unexpected error has occurred: ValueError: ohai\n' + 'Failed to write to log at ', + ) + + # our cap_out mock is imperfect so the rest of the output goes to capsys + out, _ = capsys.readouterr() + # the things that normally go to the log file will end up here + assert '### version information' in out From 68510596d31963c078bcec94e2b28b8b03b795c3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 24 Jul 2020 15:30:36 -0700 Subject: [PATCH 401/967] warn on old list-style configuration --- pre_commit/clientlib.py | 45 +++++++++++++++++++++++++---------------- pre_commit/color.py | 10 +++++++++ pre_commit/main.py | 35 ++++++++++++-------------------- tests/clientlib_test.py | 9 ++++++++- 4 files changed, 59 insertions(+), 40 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 56ec0dd1b..8dfa9473e 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -12,8 +12,10 @@ from identify.identify import ALL_TAGS import pre_commit.constants as C +from pre_commit.color import add_color_option from pre_commit.error_handler import FatalError from pre_commit.languages.all import all_languages +from pre_commit.logging_handler import logging_handler from pre_commit.util import parse_version from pre_commit.util import yaml_load @@ -43,6 +45,7 @@ def _make_argparser(filenames_help: str) -> argparse.ArgumentParser: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*', help=filenames_help) parser.add_argument('-V', '--version', action='version', version=C.VERSION) + add_color_option(parser) return parser @@ -92,14 +95,16 @@ class InvalidManifestError(FatalError): def validate_manifest_main(argv: Optional[Sequence[str]] = None) -> int: parser = _make_argparser('Manifest filenames.') args = parser.parse_args(argv) - ret = 0 - for filename in args.filenames: - try: - load_manifest(filename) - except InvalidManifestError as e: - print(e) - ret = 1 - return ret + + with logging_handler(args.color): + ret = 0 + for filename in args.filenames: + try: + load_manifest(filename) + except InvalidManifestError as e: + print(e) + ret = 1 + return ret LOCAL = 'local' @@ -290,7 +295,11 @@ class InvalidConfigError(FatalError): def ordered_load_normalize_legacy_config(contents: str) -> Dict[str, Any]: data = yaml_load(contents) if isinstance(data, list): - # TODO: Once happy, issue a deprecation warning and instructions + logger.warning( + 'normalizing pre-commit configuration to a top-level map. ' + 'support for top level list will be removed in a future version. ' + 'run: `pre-commit migrate-config` to automatically fix this.', + ) return {'repos': data} else: return data @@ -307,11 +316,13 @@ def ordered_load_normalize_legacy_config(contents: str) -> Dict[str, Any]: def validate_config_main(argv: Optional[Sequence[str]] = None) -> int: parser = _make_argparser('Config filenames.') args = parser.parse_args(argv) - ret = 0 - for filename in args.filenames: - try: - load_config(filename) - except InvalidConfigError as e: - print(e) - ret = 1 - return ret + + with logging_handler(args.color): + ret = 0 + for filename in args.filenames: + try: + load_config(filename) + except InvalidConfigError as e: + print(e) + ret = 1 + return ret diff --git a/pre_commit/color.py b/pre_commit/color.py index eb906b78f..4ddfdf5b3 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -1,3 +1,4 @@ +import argparse import os import sys @@ -95,3 +96,12 @@ def use_color(setting: str) -> bool: os.getenv('TERM') != 'dumb' ) ) + + +def add_color_option(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + '--color', default=os.environ.get('PRE_COMMIT_COLOR', 'auto'), + type=use_color, + metavar='{' + ','.join(COLOR_CHOICES) + '}', + help='Whether to use color in output. Defaults to `%(default)s`.', + ) diff --git a/pre_commit/main.py b/pre_commit/main.py index ffcc2e875..86479607c 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -8,8 +8,8 @@ from typing import Union import pre_commit.constants as C -from pre_commit import color from pre_commit import git +from pre_commit.color import add_color_option from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.clean import clean from pre_commit.commands.gc import gc @@ -41,15 +41,6 @@ COMMANDS_NO_GIT = {'clean', 'gc', 'init-templatedir', 'sample-config'} -def _add_color_option(parser: argparse.ArgumentParser) -> None: - parser.add_argument( - '--color', default=os.environ.get('PRE_COMMIT_COLOR', 'auto'), - type=color.use_color, - metavar='{' + ','.join(color.COLOR_CHOICES) + '}', - help='Whether to use color in output. Defaults to `%(default)s`.', - ) - - def _add_config_option(parser: argparse.ArgumentParser) -> None: parser.add_argument( '-c', '--config', default=C.CONFIG_FILE, @@ -195,7 +186,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: 'autoupdate', help="Auto-update pre-commit config to the latest repos' versions.", ) - _add_color_option(autoupdate_parser) + add_color_option(autoupdate_parser) _add_config_option(autoupdate_parser) autoupdate_parser.add_argument( '--bleeding-edge', action='store_true', @@ -216,11 +207,11 @@ def main(argv: Optional[Sequence[str]] = None) -> int: clean_parser = subparsers.add_parser( 'clean', help='Clean out pre-commit files.', ) - _add_color_option(clean_parser) + add_color_option(clean_parser) _add_config_option(clean_parser) hook_impl_parser = subparsers.add_parser('hook-impl') - _add_color_option(hook_impl_parser) + add_color_option(hook_impl_parser) _add_config_option(hook_impl_parser) hook_impl_parser.add_argument('--hook-type') hook_impl_parser.add_argument('--hook-dir') @@ -230,7 +221,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: hook_impl_parser.add_argument(dest='rest', nargs=argparse.REMAINDER) gc_parser = subparsers.add_parser('gc', help='Clean unused cached repos.') - _add_color_option(gc_parser) + add_color_option(gc_parser) _add_config_option(gc_parser) init_templatedir_parser = subparsers.add_parser( @@ -240,7 +231,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: '`git config init.templateDir`.' ), ) - _add_color_option(init_templatedir_parser) + add_color_option(init_templatedir_parser) _add_config_option(init_templatedir_parser) init_templatedir_parser.add_argument( 'directory', help='The directory in which to write the hook script.', @@ -256,7 +247,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: install_parser = subparsers.add_parser( 'install', help='Install the pre-commit script.', ) - _add_color_option(install_parser) + add_color_option(install_parser) _add_config_option(install_parser) install_parser.add_argument( '-f', '--overwrite', action='store_true', @@ -286,32 +277,32 @@ def main(argv: Optional[Sequence[str]] = None) -> int: 'useful.' ), ) - _add_color_option(install_hooks_parser) + add_color_option(install_hooks_parser) _add_config_option(install_hooks_parser) migrate_config_parser = subparsers.add_parser( 'migrate-config', help='Migrate list configuration to new map configuration.', ) - _add_color_option(migrate_config_parser) + add_color_option(migrate_config_parser) _add_config_option(migrate_config_parser) run_parser = subparsers.add_parser('run', help='Run hooks.') - _add_color_option(run_parser) + add_color_option(run_parser) _add_config_option(run_parser) _add_run_options(run_parser) sample_config_parser = subparsers.add_parser( 'sample-config', help=f'Produce a sample {C.CONFIG_FILE} file', ) - _add_color_option(sample_config_parser) + add_color_option(sample_config_parser) _add_config_option(sample_config_parser) try_repo_parser = subparsers.add_parser( 'try-repo', help='Try the hooks in a repository, useful for developing new hooks.', ) - _add_color_option(try_repo_parser) + add_color_option(try_repo_parser) _add_config_option(try_repo_parser) try_repo_parser.add_argument( 'repo', help='Repository to source hooks from.', @@ -328,7 +319,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: uninstall_parser = subparsers.add_parser( 'uninstall', help='Uninstall the pre-commit script.', ) - _add_color_option(uninstall_parser) + add_color_option(uninstall_parser) _add_config_option(uninstall_parser) _add_hook_type_option(uninstall_parser) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index c48adbde9..2e2f738c9 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -30,6 +30,10 @@ def test_check_type_tag_failures(value): check_type_tag(value) +def test_check_type_tag_success(): + check_type_tag('file') + + @pytest.mark.parametrize( ('config_obj', 'expected'), ( ( @@ -110,15 +114,18 @@ def test_validate_config_main_ok(): assert not validate_config_main(('.pre-commit-config.yaml',)) -def test_validate_config_old_list_format_ok(tmpdir): +def test_validate_config_old_list_format_ok(tmpdir, cap_out): f = tmpdir.join('cfg.yaml') f.write('- {repo: meta, hooks: [{id: identity}]}') assert not validate_config_main((f.strpath,)) + start = '[WARNING] normalizing pre-commit configuration to a top-level map' + assert cap_out.get().startswith(start) def test_validate_warn_on_unknown_keys_at_repo_level(tmpdir, caplog): f = tmpdir.join('cfg.yaml') f.write( + 'repos:\n' '- repo: https://gitlab.com/pycqa/flake8\n' ' rev: 3.7.7\n' ' hooks:\n' From 4063730925f19c860c2f402bf86b733ba97c2630 Mon Sep 17 00:00:00 2001 From: Johan Henkens Date: Fri, 21 Aug 2020 20:40:59 -0700 Subject: [PATCH 402/967] Save diff between hook executions --- pre_commit/commands/run.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 567b7cd3b..1f28c8c74 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -134,9 +134,10 @@ def _run_single_hook( hook: Hook, skips: Set[str], cols: int, + diff_before: bytes, verbose: bool, use_color: bool, -) -> bool: +) -> Tuple[bool, bytes]: filenames = classifier.filenames_for_hook(hook) if hook.id in skips or hook.alias in skips: @@ -151,6 +152,7 @@ def _run_single_hook( ) duration = None retcode = 0 + diff_after = diff_before files_modified = False out = b'' elif not filenames and not hook.always_run: @@ -166,21 +168,20 @@ def _run_single_hook( ) duration = None retcode = 0 + diff_after = diff_before files_modified = False out = b'' else: # print hook and dots first in case the hook takes a while to run output.write(_start_msg(start=hook.name, end_len=6, cols=cols)) - diff_cmd = ('git', 'diff', '--no-ext-diff') - diff_before = cmd_output_b(*diff_cmd, retcode=None) if not hook.pass_filenames: filenames = () time_before = time.time() language = languages[hook.language] retcode, out = language.run_hook(hook, filenames, use_color) duration = round(time.time() - time_before, 2) or 0 - diff_after = cmd_output_b(*diff_cmd, retcode=None) + diff_after = _get_diff() # if the hook makes changes, fail the commit files_modified = diff_before != diff_after @@ -212,7 +213,7 @@ def _run_single_hook( output.write_line_b(out.strip(), logfile_name=hook.log_file) output.write_line() - return files_modified or bool(retcode) + return files_modified or bool(retcode), diff_after def _compute_cols(hooks: Sequence[Hook]) -> int: @@ -248,6 +249,11 @@ def _all_filenames(args: argparse.Namespace) -> Collection[str]: return git.get_staged_files() +def _get_diff() -> bytes: + _, out, _ = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None) + return out + + def _run_hooks( config: Dict[str, Any], hooks: Sequence[Hook], @@ -261,14 +267,16 @@ def _run_hooks( _all_filenames(args), config['files'], config['exclude'], ) retval = 0 + prior_diff = _get_diff() for hook in hooks: - retval |= _run_single_hook( - classifier, hook, skips, cols, + current_retval, prior_diff = _run_single_hook( + classifier, hook, skips, cols, prior_diff, verbose=args.verbose, use_color=args.color, ) + retval |= current_retval if retval and config['fail_fast']: break - if retval and args.show_diff_on_failure and git.has_diff(): + if retval and args.show_diff_on_failure and prior_diff: if args.all_files: output.write_line( 'pre-commit hook(s) made changes.\n' From bf33f4c91c2e73bac36ec37a3dcf92bef8f0492c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 21 Aug 2020 23:11:30 -0700 Subject: [PATCH 403/967] allow pre-commit to succeed on a readonly store directory --- pre_commit/store.py | 6 ++++++ tests/store_test.py | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/pre_commit/store.py b/pre_commit/store.py index 6d8c40a93..809a6f4dc 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -43,6 +43,10 @@ class Store: def __init__(self, directory: Optional[str] = None) -> None: self.directory = directory or Store.get_default_directory() self.db_path = os.path.join(self.directory, 'db.db') + self.readonly = ( + os.path.exists(self.directory) and + not os.access(self.directory, os.W_OK) + ) if not os.path.exists(self.directory): os.makedirs(self.directory, exist_ok=True) @@ -218,6 +222,8 @@ def _create_config_table(self, db: sqlite3.Connection) -> None: ) def mark_config_used(self, path: str) -> None: + if self.readonly: # pragma: win32 no cover + return path = os.path.realpath(path) # don't insert config files that do not exist if not os.path.exists(path): diff --git a/tests/store_test.py b/tests/store_test.py index 6a4e900c9..0947144ed 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -1,5 +1,6 @@ import os.path import sqlite3 +import stat from unittest import mock import pytest @@ -12,6 +13,7 @@ from testing.fixtures import git_dir from testing.util import cwd from testing.util import git_commit +from testing.util import xfailif_windows def test_our_session_fixture_works(): @@ -217,3 +219,27 @@ def test_select_all_configs_roll_forward(store): def test_mark_config_as_used_roll_forward(store, tmpdir): _simulate_pre_1_14_0(store) test_mark_config_as_used(store, tmpdir) + + +@xfailif_windows # pragma: win32 no cover +def test_mark_config_as_used_readonly(tmpdir): + cfg = tmpdir.join('f').ensure() + store_dir = tmpdir.join('store') + # make a store, then we'll convert its directory to be readonly + assert not Store(str(store_dir)).readonly # directory didn't exist + assert not Store(str(store_dir)).readonly # directory did exist + + def _chmod_minus_w(p): + st = os.stat(p) + os.chmod(p, st.st_mode & ~(stat.S_IWUSR | stat.S_IWOTH | stat.S_IWGRP)) + + _chmod_minus_w(store_dir) + for fname in os.listdir(store_dir): + assert not os.path.isdir(fname) + _chmod_minus_w(os.path.join(store_dir, fname)) + + store = Store(str(store_dir)) + assert store.readonly + # should be skipped due to readonly + store.mark_config_used(str(cfg)) + assert store.select_all_configs() == [] From f1de792877f904b7349d3ae163a3694f2854ade1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 22 Aug 2020 13:31:12 -0700 Subject: [PATCH 404/967] v2.7.0 --- CHANGELOG.md | 23 +++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c487acf6d..e692f3dce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +2.7.0 - 2020-08-22 +================== + +### Features +- Produce error message if an environment is immediately unhealthy + - #1535 PR by @asottile. +- Add --no-allow-missing-config option to init-templatedir + - #1539 PR by @singergr. +- Add warning for old list-style configuration + - #1544 PR by @asottile. +- Allow pre-commit to succeed on a readonly store. + - #1570 PR by @asottile. + - #1536 issue by @asottile. + +### Fixes +- Fix error messaging when the store directory is readonly + - #1546 PR by @asottile. + - #1536 issue by @asottile. +- Improve `diff` performance with many hooks + - #1566 PR by @jhenkens. + - #1564 issue by @jhenkens. + + 2.6.0 - 2020-07-01 ================== diff --git a/setup.cfg b/setup.cfg index 0ce58b1a2..c9d7f82e7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.6.0 +version = 2.7.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From eb8b48aeb439ee69610a7c08a3a1de75fbbfe572 Mon Sep 17 00:00:00 2001 From: Ruairidh MacLeod Date: Sun, 23 Aug 2020 00:14:10 +0000 Subject: [PATCH 405/967] remove docker_is_running check from source Moved to testing.util so it can be used for the skipif_cant_run_docker test hooks. --- pre_commit/languages/docker.py | 19 ------------------- pre_commit/languages/docker_image.py | 2 -- testing/util.py | 12 +++++++++++- tests/languages/docker_test.py | 9 --------- 4 files changed, 11 insertions(+), 31 deletions(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 4091492cc..9c1311988 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -7,9 +7,7 @@ from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix -from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure -from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'docker' PRE_COMMIT_LABEL = 'PRE_COMMIT' @@ -26,21 +24,6 @@ def docker_tag(prefix: Prefix) -> str: # pragma: win32 no cover return f'pre-commit-{md5sum}' -def docker_is_running() -> bool: # pragma: win32 no cover - try: - cmd_output_b('docker', 'ps') - except CalledProcessError: - return False - else: - return True - - -def assert_docker_available() -> None: # pragma: win32 no cover - assert docker_is_running(), ( - 'Docker is either not running or not configured in this environment' - ) - - def build_docker_image( prefix: Prefix, *, @@ -63,7 +46,6 @@ def install_environment( ) -> None: # pragma: win32 no cover helpers.assert_version_default('docker', version) helpers.assert_no_additional_deps('docker', additional_dependencies) - assert_docker_available() directory = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), @@ -101,7 +83,6 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: # pragma: win32 no cover - assert_docker_available() # Rebuild the docker image in case it has gone missing, as many people do # automated cleanup of docker images. build_docker_image(hook.prefix, pull=False) diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index 0c51df628..311d1277d 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -3,7 +3,6 @@ from pre_commit.hook import Hook from pre_commit.languages import helpers -from pre_commit.languages.docker import assert_docker_available from pre_commit.languages.docker import docker_cmd ENVIRONMENT_DIR = None @@ -17,6 +16,5 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: # pragma: win32 no cover - assert_docker_available() cmd = docker_cmd() + hook.cmd return helpers.run_xargs(hook, cmd, file_args, color=color) diff --git a/testing/util.py b/testing/util.py index 4edb7a9ec..f556a8dd9 100644 --- a/testing/util.py +++ b/testing/util.py @@ -5,14 +5,24 @@ import pytest from pre_commit import parse_shebang -from pre_commit.languages.docker import docker_is_running +from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b from testing.auto_namedtuple import auto_namedtuple TESTING_DIR = os.path.abspath(os.path.dirname(__file__)) +def docker_is_running() -> bool: # pragma: win32 no cover + try: + cmd_output_b('docker', 'ps') + except CalledProcessError: # pragma: no cover + return False + else: + return True + + def get_resource_path(path): return os.path.join(TESTING_DIR, 'resources', path) diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index b65b2235a..3bed4bfa5 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -1,15 +1,6 @@ from unittest import mock from pre_commit.languages import docker -from pre_commit.util import CalledProcessError - - -def test_docker_is_running_process_error(): - with mock.patch( - 'pre_commit.languages.docker.cmd_output_b', - side_effect=CalledProcessError(1, (), 0, b'', None), - ): - assert docker.docker_is_running() is False def test_docker_fallback_user(): From b63b37ac36caf89de55a7ae45bb57d981b1b1e36 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 23 Aug 2020 09:55:29 -0700 Subject: [PATCH 406/967] fix cache of invalidated unhealthy environment version info --- pre_commit/languages/python.py | 3 ++- tests/languages/python_test.py | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 6f7c90055..7a685808e 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -191,7 +191,8 @@ def healthy(prefix: Prefix, language_version: str) -> bool: return ( 'version_info' in cfg and - _version_info(py_exe) == cfg['version_info'] and ( + # always use uncached lookup here in case we replaced an unhealthy env + _version_info.__wrapped__(py_exe) == cfg['version_info'] and ( 'base-executable' not in cfg or _version_info(cfg['base-executable']) == cfg['version_info'] ) diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index c419ad621..29c5a9bf2 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -8,6 +8,7 @@ from pre_commit.envcontext import envcontext from pre_commit.languages import python from pre_commit.prefix import Prefix +from pre_commit.util import make_executable def test_read_pyvenv_cfg(tmpdir): @@ -141,3 +142,26 @@ def test_unhealthy_old_virtualenv(python_dir): os.remove(prefix.path('py_env-default/pyvenv.cfg')) assert python.healthy(prefix, C.DEFAULT) is False + + +def test_unhealthy_then_replaced(python_dir): + prefix, tmpdir = python_dir + + python.install_environment(prefix, C.DEFAULT, ()) + + # simulate an exe which returns an old version + exe_name = 'python.exe' if sys.platform == 'win32' else 'python' + py_exe = prefix.path(python.bin_dir('py_env-default'), exe_name) + os.rename(py_exe, f'{py_exe}.tmp') + + with open(py_exe, 'w') as f: + f.write('#!/usr/bin/env bash\necho 1.2.3\n') + make_executable(py_exe) + + # should be unhealthy due to version mismatch + assert python.healthy(prefix, C.DEFAULT) is False + + # now put the exe back and it should be healthy again + os.replace(f'{py_exe}.tmp', py_exe) + + assert python.healthy(prefix, C.DEFAULT) is True From 79b098c409460166d810c51a3048ba427a0e9e80 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 23 Aug 2020 10:18:59 -0700 Subject: [PATCH 407/967] fix atomic file replace on windows --- pre_commit/commands/install_uninstall.py | 2 +- pre_commit/repository.py | 2 +- pre_commit/store.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index c8b7633b6..85fa53cbb 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -165,7 +165,7 @@ def _uninstall_hook_script(hook_type: str) -> None: output.write_line(f'{hook_type} uninstalled') if os.path.exists(legacy_path): - os.rename(legacy_path, hook_path) + os.replace(legacy_path, hook_path) output.write_line(f'Restored previous hooks to {hook_path}') diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 91c430555..46e96c1dc 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -48,7 +48,7 @@ def _write_state(prefix: Prefix, venv: str, state: object) -> None: with open(staging, 'w') as state_file: state_file.write(json.dumps(state)) # Move the file into place atomically to indicate we've installed - os.rename(staging, state_filename) + os.replace(staging, state_filename) def _hook_installed(hook: Hook) -> bool: diff --git a/pre_commit/store.py b/pre_commit/store.py index 809a6f4dc..e5522ec33 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -79,7 +79,7 @@ def __init__(self, directory: Optional[str] = None) -> None: self._create_config_table(db) # Atomic file move - os.rename(tmpfile, self.db_path) + os.replace(tmpfile, self.db_path) @contextlib.contextmanager def exclusive_lock(self) -> Generator[None, None, None]: From f511afe40e3f0ea7474d37f19c69741d3e167876 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 23 Aug 2020 10:53:21 -0700 Subject: [PATCH 408/967] v2.7.1 --- CHANGELOG.md | 14 ++++++++++++++ setup.cfg | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e692f3dce..a92a6b36c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +2.7.1 - 2020-08-23 +================== + +### Fixes +- Improve performance of docker hooks by removing slow `ps` call + - #1572 PR by @rkm. + - #1569 issue by @asottile. +- Fix un-`healthy()` invalidation followed by install being reported as + un-`healthy()`. + - #1576 PR by @asottile. + - #1575 issue by @jab. +- Fix rare file race condition on windows with `os.replace()` + - #1577 PR by @asottile. + 2.7.0 - 2020-08-22 ================== diff --git a/setup.cfg b/setup.cfg index c9d7f82e7..4153d7650 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.7.0 +version = 2.7.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From b149c7a344a407fb3c9c8c99b9647c3c95f1a998 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 7 Sep 2020 13:23:02 -0700 Subject: [PATCH 409/967] fix for node healthy() when system executable moves --- pre_commit/languages/node.py | 7 ++++++- tests/languages/node_test.py | 37 ++++++++++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index d99e6f2c7..dccbb7ca2 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -21,7 +21,6 @@ from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'node_env' -healthy = helpers.basic_healthy @functools.lru_cache(maxsize=1) @@ -73,6 +72,12 @@ def in_env( yield +def healthy(prefix: Prefix, language_version: str) -> bool: + with in_env(prefix, language_version): + retcode, _, _ = cmd_output_b('node', '--version', retcode=None) + return retcode == 0 + + def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: diff --git a/tests/languages/node_test.py b/tests/languages/node_test.py index fd300469a..c8e2d47d1 100644 --- a/tests/languages/node_test.py +++ b/tests/languages/node_test.py @@ -1,14 +1,19 @@ +import os +import shutil import sys from unittest import mock import pytest import pre_commit.constants as C +from pre_commit import envcontext from pre_commit import parse_shebang -from pre_commit.languages.node import get_default_version +from pre_commit.languages import node +from pre_commit.prefix import Prefix +from testing.util import xfailif_windows -ACTUAL_GET_DEFAULT_VERSION = get_default_version.__wrapped__ +ACTUAL_GET_DEFAULT_VERSION = node.get_default_version.__wrapped__ @pytest.fixture @@ -45,3 +50,31 @@ def test_uses_default_when_node_and_npm_are_not_available(find_exe_mck): def test_sets_default_on_windows(find_exe_mck): find_exe_mck.return_value = '/path/to/exe' assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT + + +@xfailif_windows # pragma: win32 no cover +def test_healthy_system_node(tmpdir): + tmpdir.join('package.json').write('{"name": "t", "version": "1.0.0"}') + + prefix = Prefix(str(tmpdir)) + node.install_environment(prefix, 'system', ()) + assert node.healthy(prefix, 'system') + + +@xfailif_windows # pragma: win32 no cover +def test_unhealthy_if_system_node_goes_missing(tmpdir): + bin_dir = tmpdir.join('bin').ensure_dir() + node_bin = bin_dir.join('node') + node_bin.mksymlinkto(shutil.which('node')) + + prefix_dir = tmpdir.join('prefix').ensure_dir() + prefix_dir.join('package.json').write('{"name": "t", "version": "1.0.0"}') + + path = ('PATH', (str(bin_dir), os.pathsep, envcontext.Var('PATH'))) + with envcontext.envcontext((path,)): + prefix = Prefix(str(prefix_dir)) + node.install_environment(prefix, 'system', ()) + assert node.healthy(prefix, 'system') + + node_bin.remove() + assert not node.healthy(prefix, 'system') From 3a0406847b16e0f8950f90f894a6fce5dcd72813 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 7 Sep 2020 15:01:50 -0700 Subject: [PATCH 410/967] fix excess whitespace in traceback in error --- pre_commit/error_handler.py | 2 +- requirements-dev.txt | 1 + tests/commands/install_uninstall_test.py | 50 ++++++++++++------------ tests/commands/try_repo_test.py | 14 ++++--- tests/error_handler_test.py | 34 +++++++++------- tests/repository_test.py | 6 +-- 6 files changed, 59 insertions(+), 48 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 13d78cbbd..009f6d9c2 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -52,7 +52,7 @@ def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None: _log_line('```') _log_line() _log_line('```') - _log_line(formatted) + _log_line(formatted.rstrip()) _log_line('```') raise SystemExit(1) diff --git a/requirements-dev.txt b/requirements-dev.txt index d6a13dc43..14ada96ed 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,3 +2,4 @@ covdefaults coverage pytest pytest-env +re-assert diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 5809a3f27..481a7279d 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -3,6 +3,8 @@ import sys from unittest import mock +import re_assert + import pre_commit.constants as C from pre_commit import git from pre_commit.commands import install_uninstall @@ -143,7 +145,7 @@ def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): ) -NORMAL_PRE_COMMIT_RUN = re.compile( +NORMAL_PRE_COMMIT_RUN = re_assert.Matches( fr'^\[INFO\] Initializing environment for .+\.\n' fr'Bash hook\.+Passed\n' fr'\[master [a-f0-9]{{7}}\] commit!\n' @@ -159,7 +161,7 @@ def test_install_pre_commit_and_run(tempdir_factory, store): ret, output = _get_commit_output(tempdir_factory) assert ret == 0 - assert NORMAL_PRE_COMMIT_RUN.match(output) + NORMAL_PRE_COMMIT_RUN.assert_matches(output) def test_install_pre_commit_and_run_custom_path(tempdir_factory, store): @@ -171,7 +173,7 @@ def test_install_pre_commit_and_run_custom_path(tempdir_factory, store): ret, output = _get_commit_output(tempdir_factory) assert ret == 0 - assert NORMAL_PRE_COMMIT_RUN.match(output) + NORMAL_PRE_COMMIT_RUN.assert_matches(output) def test_install_in_submodule_and_run(tempdir_factory, store): @@ -185,7 +187,7 @@ def test_install_in_submodule_and_run(tempdir_factory, store): assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 - assert NORMAL_PRE_COMMIT_RUN.match(output) + NORMAL_PRE_COMMIT_RUN.assert_matches(output) def test_install_in_worktree_and_run(tempdir_factory, store): @@ -198,7 +200,7 @@ def test_install_in_worktree_and_run(tempdir_factory, store): assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 - assert NORMAL_PRE_COMMIT_RUN.match(output) + NORMAL_PRE_COMMIT_RUN.assert_matches(output) def test_commit_am(tempdir_factory, store): @@ -243,7 +245,7 @@ def test_install_idempotent(tempdir_factory, store): ret, output = _get_commit_output(tempdir_factory) assert ret == 0 - assert NORMAL_PRE_COMMIT_RUN.match(output) + NORMAL_PRE_COMMIT_RUN.assert_matches(output) def _path_without_us(): @@ -297,7 +299,7 @@ def test_environment_not_sourced(tempdir_factory, store): ) -FAILING_PRE_COMMIT_RUN = re.compile( +FAILING_PRE_COMMIT_RUN = re_assert.Matches( r'^\[INFO\] Initializing environment for .+\.\n' r'Failing hook\.+Failed\n' r'- hook id: failing_hook\n' @@ -316,10 +318,10 @@ def test_failing_hooks_returns_nonzero(tempdir_factory, store): ret, output = _get_commit_output(tempdir_factory) assert ret == 1 - assert FAILING_PRE_COMMIT_RUN.match(output) + FAILING_PRE_COMMIT_RUN.assert_matches(output) -EXISTING_COMMIT_RUN = re.compile( +EXISTING_COMMIT_RUN = re_assert.Matches( fr'^legacy hook\n' fr'\[master [a-f0-9]{{7}}\] commit!\n' fr'{FILES_CHANGED}' @@ -342,7 +344,7 @@ def test_install_existing_hooks_no_overwrite(tempdir_factory, store): # Make sure we installed the "old" hook correctly ret, output = _get_commit_output(tempdir_factory, touch_file='baz') assert ret == 0 - assert EXISTING_COMMIT_RUN.match(output) + EXISTING_COMMIT_RUN.assert_matches(output) # Now install pre-commit (no-overwrite) assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 @@ -351,7 +353,7 @@ def test_install_existing_hooks_no_overwrite(tempdir_factory, store): ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert output.startswith('legacy hook\n') - assert NORMAL_PRE_COMMIT_RUN.match(output[len('legacy hook\n'):]) + NORMAL_PRE_COMMIT_RUN.assert_matches(output[len('legacy hook\n'):]) def test_legacy_overwriting_legacy_hook(tempdir_factory, store): @@ -377,10 +379,10 @@ def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory, store): ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert output.startswith('legacy hook\n') - assert NORMAL_PRE_COMMIT_RUN.match(output[len('legacy hook\n'):]) + NORMAL_PRE_COMMIT_RUN.assert_matches(output[len('legacy hook\n'):]) -FAIL_OLD_HOOK = re.compile( +FAIL_OLD_HOOK = re_assert.Matches( r'fail!\n' r'\[INFO\] Initializing environment for .+\.\n' r'Bash hook\.+Passed\n', @@ -401,7 +403,7 @@ def test_failing_existing_hook_returns_1(tempdir_factory, store): # We should get a failure from the legacy hook ret, output = _get_commit_output(tempdir_factory) assert ret == 1 - assert FAIL_OLD_HOOK.match(output) + FAIL_OLD_HOOK.assert_matches(output) def test_install_overwrite_no_existing_hooks(tempdir_factory, store): @@ -413,7 +415,7 @@ def test_install_overwrite_no_existing_hooks(tempdir_factory, store): ret, output = _get_commit_output(tempdir_factory) assert ret == 0 - assert NORMAL_PRE_COMMIT_RUN.match(output) + NORMAL_PRE_COMMIT_RUN.assert_matches(output) def test_install_overwrite(tempdir_factory, store): @@ -426,7 +428,7 @@ def test_install_overwrite(tempdir_factory, store): ret, output = _get_commit_output(tempdir_factory) assert ret == 0 - assert NORMAL_PRE_COMMIT_RUN.match(output) + NORMAL_PRE_COMMIT_RUN.assert_matches(output) def test_uninstall_restores_legacy_hooks(tempdir_factory, store): @@ -441,7 +443,7 @@ def test_uninstall_restores_legacy_hooks(tempdir_factory, store): # Make sure we installed the "old" hook correctly ret, output = _get_commit_output(tempdir_factory, touch_file='baz') assert ret == 0 - assert EXISTING_COMMIT_RUN.match(output) + EXISTING_COMMIT_RUN.assert_matches(output) def test_replace_old_commit_script(tempdir_factory, store): @@ -463,7 +465,7 @@ def test_replace_old_commit_script(tempdir_factory, store): ret, output = _get_commit_output(tempdir_factory) assert ret == 0 - assert NORMAL_PRE_COMMIT_RUN.match(output) + NORMAL_PRE_COMMIT_RUN.assert_matches(output) def test_uninstall_doesnt_remove_not_our_hooks(in_git_dir): @@ -476,7 +478,7 @@ def test_uninstall_doesnt_remove_not_our_hooks(in_git_dir): assert pre_commit.exists() -PRE_INSTALLED = re.compile( +PRE_INSTALLED = re_assert.Matches( fr'Bash hook\.+Passed\n' fr'\[master [a-f0-9]{{7}}\] commit!\n' fr'{FILES_CHANGED}' @@ -493,7 +495,7 @@ def test_installs_hooks_with_hooks_True(tempdir_factory, store): ) assert ret == 0 - assert PRE_INSTALLED.match(output) + PRE_INSTALLED.assert_matches(output) def test_install_hooks_command(tempdir_factory, store): @@ -506,7 +508,7 @@ def test_install_hooks_command(tempdir_factory, store): ) assert ret == 0 - assert PRE_INSTALLED.match(output) + PRE_INSTALLED.assert_matches(output) def test_installed_from_venv(tempdir_factory, store): @@ -533,7 +535,7 @@ def test_installed_from_venv(tempdir_factory, store): }, ) assert ret == 0 - assert NORMAL_PRE_COMMIT_RUN.match(output) + NORMAL_PRE_COMMIT_RUN.assert_matches(output) def _get_push_output(tempdir_factory, remote='origin', opts=()): @@ -880,7 +882,7 @@ def test_prepare_commit_msg_legacy( def test_pre_merge_commit_integration(tempdir_factory, store): - expected = re.compile( + output_pattern = re_assert.Matches( r'^\[INFO\] Initializing environment for .+\n' r'Bash hook\.+Passed\n' r"Merge made by the 'recursive' strategy.\n" @@ -902,7 +904,7 @@ def test_pre_merge_commit_integration(tempdir_factory, store): tempdir_factory=tempdir_factory, ) assert ret == 0 - assert expected.match(output) + output_pattern.assert_matches(output) def test_install_disallow_missing_config(tempdir_factory, store): diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index d3ec3fda2..a157d1636 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -3,6 +3,8 @@ import time from unittest import mock +import re_assert + from pre_commit import git from pre_commit.commands.try_repo import try_repo from pre_commit.util import cmd_output @@ -43,7 +45,7 @@ def test_try_repo_repo_only(cap_out, tempdir_factory): _run_try_repo(tempdir_factory, verbose=True) start, config, rest = _get_out(cap_out) assert start == '' - assert re.match( + config_pattern = re_assert.Matches( '^repos:\n' '- repo: .+\n' ' rev: .+\n' @@ -51,8 +53,8 @@ def test_try_repo_repo_only(cap_out, tempdir_factory): ' - id: bash_hook\n' ' - id: bash_hook2\n' ' - id: bash_hook3\n$', - config, ) + config_pattern.assert_matches(config) assert rest == '''\ Bash hook............................................(no files to check)Skipped - hook id: bash_hook @@ -71,14 +73,14 @@ def test_try_repo_with_specific_hook(cap_out, tempdir_factory): _run_try_repo(tempdir_factory, hook='bash_hook', verbose=True) start, config, rest = _get_out(cap_out) assert start == '' - assert re.match( + config_pattern = re_assert.Matches( '^repos:\n' '- repo: .+\n' ' rev: .+\n' ' hooks:\n' ' - id: bash_hook\n$', - config, ) + config_pattern.assert_matches(config) assert rest == '''\ Bash hook............................................(no files to check)Skipped - hook id: bash_hook @@ -128,14 +130,14 @@ def test_try_repo_uncommitted_changes(cap_out, tempdir_factory): start, config, rest = _get_out(cap_out) assert start == '[WARNING] Creating temporary repo with uncommitted changes...\n' # noqa: E501 - assert re.match( + config_pattern = re_assert.Matches( '^repos:\n' '- repo: .+shadow-repo\n' ' rev: .+\n' ' hooks:\n' ' - id: bash_hook\n$', - config, ) + config_pattern.assert_matches(config) assert rest == 'modified name!...........................................................Passed\n' # noqa: E501 diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index d066e5728..5dc085059 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -1,10 +1,10 @@ import os.path -import re import stat import sys from unittest import mock import pytest +import re_assert from pre_commit import error_handler from pre_commit.store import Store @@ -37,7 +37,7 @@ def test_error_handler_fatal_error(mocked_log_and_exit): mock.ANY, ) - assert re.match( + pattern = re_assert.Matches( r'Traceback \(most recent call last\):\n' r' File ".+pre_commit.error_handler.py", line \d+, in error_handler\n' r' yield\n' @@ -45,8 +45,8 @@ def test_error_handler_fatal_error(mocked_log_and_exit): r'in test_error_handler_fatal_error\n' r' raise exc\n' r'(pre_commit\.error_handler\.)?FatalError: just a test\n', - mocked_log_and_exit.call_args[0][2], ) + pattern.assert_matches(mocked_log_and_exit.call_args[0][2]) def test_error_handler_uncaught_error(mocked_log_and_exit): @@ -60,7 +60,7 @@ def test_error_handler_uncaught_error(mocked_log_and_exit): # Tested below mock.ANY, ) - assert re.match( + pattern = re_assert.Matches( r'Traceback \(most recent call last\):\n' r' File ".+pre_commit.error_handler.py", line \d+, in error_handler\n' r' yield\n' @@ -68,8 +68,8 @@ def test_error_handler_uncaught_error(mocked_log_and_exit): r'in test_error_handler_uncaught_error\n' r' raise exc\n' r'ValueError: another test\n', - mocked_log_and_exit.call_args[0][2], ) + pattern.assert_matches(mocked_log_and_exit.call_args[0][2]) def test_error_handler_keyboardinterrupt(mocked_log_and_exit): @@ -83,7 +83,7 @@ def test_error_handler_keyboardinterrupt(mocked_log_and_exit): # Tested below mock.ANY, ) - assert re.match( + pattern = re_assert.Matches( r'Traceback \(most recent call last\):\n' r' File ".+pre_commit.error_handler.py", line \d+, in error_handler\n' r' yield\n' @@ -91,15 +91,19 @@ def test_error_handler_keyboardinterrupt(mocked_log_and_exit): r'in test_error_handler_keyboardinterrupt\n' r' raise exc\n' r'KeyboardInterrupt\n', - mocked_log_and_exit.call_args[0][2], ) + pattern.assert_matches(mocked_log_and_exit.call_args[0][2]) def test_log_and_exit(cap_out, mock_store_dir): + tb = ( + 'Traceback (most recent call last):\n' + ' File "", line 2, in \n' + 'pre_commit.error_handler.FatalError: hai\n' + ) + with pytest.raises(SystemExit): - error_handler._log_and_exit( - 'msg', error_handler.FatalError('hai'), "I'm a stacktrace", - ) + error_handler._log_and_exit('msg', error_handler.FatalError('hai'), tb) printed = cap_out.get() log_file = os.path.join(mock_store_dir, 'pre-commit.log') @@ -108,7 +112,7 @@ def test_log_and_exit(cap_out, mock_store_dir): assert os.path.exists(log_file) with open(log_file) as f: logged = f.read() - expected = ( + pattern = re_assert.Matches( r'^### version information\n' r'\n' r'```\n' @@ -127,10 +131,12 @@ def test_log_and_exit(cap_out, mock_store_dir): r'```\n' r'\n' r'```\n' - r"I'm a stacktrace\n" - r'```\n' + r'Traceback \(most recent call last\):\n' + r' File "", line 2, in \n' + r'pre_commit\.error_handler\.FatalError: hai\n' + r'```\n', ) - assert re.match(expected, logged) + pattern.assert_matches(logged) def test_error_handler_non_ascii_exception(mock_store_dir): diff --git a/tests/repository_test.py b/tests/repository_test.py index 84e4da930..035b02a65 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1,5 +1,4 @@ import os.path -import re import shutil import sys from typing import Any @@ -8,6 +7,7 @@ import cfgv import pytest +import re_assert import pre_commit.constants as C from pre_commit.clientlib import CONFIG_SCHEMA @@ -843,12 +843,12 @@ def test_too_new_version(tempdir_factory, store, fake_log_handler): with pytest.raises(SystemExit): _get_hook(config, store, 'bash_hook') msg = fake_log_handler.handle.call_args[0][0].msg - assert re.match( + pattern = re_assert.Matches( r'^The hook `bash_hook` requires pre-commit version 999\.0\.0 but ' r'version \d+\.\d+\.\d+ is installed. ' r'Perhaps run `pip install --upgrade pre-commit`\.$', - msg, ) + pattern.assert_matches(msg) @pytest.mark.parametrize('version', ('0.1.0', C.VERSION)) From 273326b89b3eb17656a46f63f094fd1c0a55af84 Mon Sep 17 00:00:00 2001 From: Celeborn2BeAlive Date: Wed, 9 Sep 2020 09:32:44 +0200 Subject: [PATCH 411/967] drop python.exe extension on windows on shebang --- pre_commit/commands/install_uninstall.py | 2 +- tests/commands/install_uninstall_test.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 85fa53cbb..684b59805 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -55,7 +55,7 @@ def is_our_script(filename: str) -> bool: def shebang() -> str: if sys.platform == 'win32': - py = SYS_EXE + py, _ = os.path.splitext(SYS_EXE) else: exe_choices = [ f'python{sys.version_info[0]}.{sys.version_info[1]}', diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 481a7279d..7a4b90635 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -56,8 +56,13 @@ def patch_sys_exe(exe): def test_shebang_windows(): + with patch_platform('win32'), patch_sys_exe('python'): + assert shebang() == '#!/usr/bin/env python' + + +def test_shebang_windows_drop_ext(): with patch_platform('win32'), patch_sys_exe('python.exe'): - assert shebang() == '#!/usr/bin/env python.exe' + assert shebang() == '#!/usr/bin/env python' def test_shebang_posix_not_on_path(): From 48886449907f4db13a8f1994340c9948623cd832 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 15 Sep 2020 12:04:25 -0700 Subject: [PATCH 412/967] remove hardcoded python location --- pre_commit/languages/python.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 7a685808e..afa093d56 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -114,11 +114,6 @@ def get_default_version() -> str: # pragma: no cover (platform dependent) if _find_by_py_launcher(exe): return exe - # Give a best-effort try for windows - default_folder_name = exe.replace('.', '') - if os.path.exists(fr'C:\{default_folder_name}\python.exe'): - return exe - # We tried! return C.DEFAULT @@ -155,12 +150,6 @@ def norm_version(version: str) -> str: if version_exec and version_exec != version: return version_exec - # If it is in the form pythonx.x search in the default - # place on windows - if version.startswith('python'): - default_folder_name = version.replace('.', '') - return fr'C:\{default_folder_name}\python.exe' - # Otherwise assume it is a path return os.path.expanduser(version) From 13eed4ac5bcb4640bb30a5368bf6324a32ee8bc7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 18 Sep 2020 09:13:19 -0700 Subject: [PATCH 413/967] Fix ruby hooks when --format-executable is in gemrc I used this gemrc to break things (default on opensuse): ```yaml --- :benchmark: false :install: --format-executable --no-user-install install: --format-executable --no-user-install :backtrace: true :update_sources: true :format_executable: true :verbose: true :update: --format-executable --no-user-install update: --format-executable --no-user-install :bulk_threshold: 1000 :sources: - https://rubygems.org ``` --- pre_commit/languages/ruby.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 73b23cc07..ef73961f1 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -134,7 +134,8 @@ def install_environment( helpers.run_setup_cmd( prefix, ( - 'gem', 'install', '--no-document', + 'gem', 'install', + '--no-document', '--no-format-executable', *prefix.star('.gem'), *additional_dependencies, ), ) From 91530f1005a559e73f269ab602b2c9bfde1a66b6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 20 Sep 2020 10:19:31 -0700 Subject: [PATCH 414/967] check cygwin mismatch earlier --- pre_commit/clientlib.py | 2 +- pre_commit/error_handler.py | 5 +---- pre_commit/errors.py | 2 ++ pre_commit/git.py | 20 ++++++++++++++++++-- pre_commit/main.py | 23 ++++------------------- tests/error_handler_test.py | 11 ++++++----- tests/main_test.py | 2 +- 7 files changed, 33 insertions(+), 32 deletions(-) create mode 100644 pre_commit/errors.py diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 8dfa9473e..87679bfa6 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -13,7 +13,7 @@ import pre_commit.constants as C from pre_commit.color import add_color_option -from pre_commit.error_handler import FatalError +from pre_commit.errors import FatalError from pre_commit.languages.all import all_languages from pre_commit.logging_handler import logging_handler from pre_commit.util import parse_version diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 009f6d9c2..afacab9bb 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -7,14 +7,11 @@ import pre_commit.constants as C from pre_commit import output +from pre_commit.errors import FatalError from pre_commit.store import Store from pre_commit.util import force_bytes -class FatalError(RuntimeError): - pass - - def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None: error_msg = f'{msg}: {type(exc).__name__}: '.encode() + force_bytes(exc) output.write_line_b(error_msg) diff --git a/pre_commit/errors.py b/pre_commit/errors.py new file mode 100644 index 000000000..f84d3f185 --- /dev/null +++ b/pre_commit/errors.py @@ -0,0 +1,2 @@ +class FatalError(RuntimeError): + pass diff --git a/pre_commit/git.py b/pre_commit/git.py index 576bef8cc..ca30eaa7e 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -6,6 +6,8 @@ from typing import Optional from typing import Set +from pre_commit.errors import FatalError +from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b from pre_commit.util import EnvironT @@ -43,7 +45,21 @@ def no_git_env(_env: Optional[EnvironT] = None) -> Dict[str, str]: def get_root() -> str: - return cmd_output('git', 'rev-parse', '--show-toplevel')[1].strip() + try: + root = cmd_output('git', 'rev-parse', '--show-toplevel')[1].strip() + except CalledProcessError: + raise FatalError( + 'git failed. Is it installed, and are you in a Git repository ' + 'directory?', + ) + else: + if root == '': # pragma: no cover (old git) + raise FatalError( + 'git toplevel unexpectedly empty! make sure you are not ' + 'inside the `.git` directory of your repository.', + ) + else: + return root def get_git_dir(git_root: str = '.') -> str: @@ -181,7 +197,7 @@ def check_for_cygwin_mismatch() -> None: """See https://github.com/pre-commit/pre-commit/issues/354""" if sys.platform in ('cygwin', 'win32'): # pragma: no cover (windows) is_cygwin_python = sys.platform == 'cygwin' - toplevel = cmd_output('git', 'rev-parse', '--show-toplevel')[1] + toplevel = get_root() is_cygwin_git = toplevel.startswith('/') if is_cygwin_python ^ is_cygwin_git: diff --git a/pre_commit/main.py b/pre_commit/main.py index 86479607c..c1eb104ac 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -23,10 +23,8 @@ from pre_commit.commands.sample_config import sample_config from pre_commit.commands.try_repo import try_repo from pre_commit.error_handler import error_handler -from pre_commit.error_handler import FatalError from pre_commit.logging_handler import logging_handler from pre_commit.store import Store -from pre_commit.util import CalledProcessError logger = logging.getLogger('pre_commit') @@ -146,21 +144,8 @@ def _adjust_args_and_chdir(args: argparse.Namespace) -> None: if args.command == 'try-repo' and os.path.exists(args.repo): args.repo = os.path.abspath(args.repo) - try: - toplevel = git.get_root() - except CalledProcessError: - raise FatalError( - 'git failed. Is it installed, and are you in a Git repository ' - 'directory?', - ) - else: - if toplevel == '': # pragma: no cover (old git) - raise FatalError( - 'git toplevel unexpectedly empty! make sure you are not ' - 'inside the `.git` directory of your repository.', - ) - else: - os.chdir(toplevel) + toplevel = git.get_root() + os.chdir(toplevel) args.config = os.path.relpath(args.config) if args.command in {'run', 'try-repo'}: @@ -339,11 +324,11 @@ def main(argv: Optional[Sequence[str]] = None) -> int: parser.parse_args(['--help']) with error_handler(), logging_handler(args.color): + git.check_for_cygwin_mismatch() + if args.command not in COMMANDS_NO_GIT: _adjust_args_and_chdir(args) - git.check_for_cygwin_mismatch() - store = Store() store.mark_config_used(args.config) diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 5dc085059..804701f05 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -7,6 +7,7 @@ import re_assert from pre_commit import error_handler +from pre_commit.errors import FatalError from pre_commit.store import Store from pre_commit.util import CalledProcessError from testing.util import cmd_output_mocked_pre_commit_home @@ -26,7 +27,7 @@ def test_error_handler_no_exception(mocked_log_and_exit): def test_error_handler_fatal_error(mocked_log_and_exit): - exc = error_handler.FatalError('just a test') + exc = FatalError('just a test') with error_handler.error_handler(): raise exc @@ -44,7 +45,7 @@ def test_error_handler_fatal_error(mocked_log_and_exit): r' File ".+tests.error_handler_test.py", line \d+, ' r'in test_error_handler_fatal_error\n' r' raise exc\n' - r'(pre_commit\.error_handler\.)?FatalError: just a test\n', + r'(pre_commit\.errors\.)?FatalError: just a test\n', ) pattern.assert_matches(mocked_log_and_exit.call_args[0][2]) @@ -99,11 +100,11 @@ def test_log_and_exit(cap_out, mock_store_dir): tb = ( 'Traceback (most recent call last):\n' ' File "", line 2, in \n' - 'pre_commit.error_handler.FatalError: hai\n' + 'pre_commit.errors.FatalError: hai\n' ) with pytest.raises(SystemExit): - error_handler._log_and_exit('msg', error_handler.FatalError('hai'), tb) + error_handler._log_and_exit('msg', FatalError('hai'), tb) printed = cap_out.get() log_file = os.path.join(mock_store_dir, 'pre-commit.log') @@ -133,7 +134,7 @@ def test_log_and_exit(cap_out, mock_store_dir): r'```\n' r'Traceback \(most recent call last\):\n' r' File "", line 2, in \n' - r'pre_commit\.error_handler\.FatalError: hai\n' + r'pre_commit\.errors\.FatalError: hai\n' r'```\n', ) pattern.assert_matches(logged) diff --git a/tests/main_test.py b/tests/main_test.py index f7abeeb4d..6738df683 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -6,7 +6,7 @@ import pre_commit.constants as C from pre_commit import main -from pre_commit.error_handler import FatalError +from pre_commit.errors import FatalError from testing.auto_namedtuple import auto_namedtuple From 365f896c36caa206ee0fd6feb9295e65e6db71bb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 20 Sep 2020 19:20:54 -0700 Subject: [PATCH 415/967] fix a few spelling errors found via `pre-commit try-repo https://github.com/codespell-project/codespell --all-files` --- CHANGELOG.md | 2 +- pre_commit/languages/conda.py | 2 +- tests/xargs_test.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a92a6b36c..1621bb3fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1108,7 +1108,7 @@ that have helped us get this far! 0.18.1 - 2017-09-04 =================== - Only mention locking when waiting for a lock. -- Fix `IOError` during locking in timeout situtation on windows under python 2. +- Fix `IOError` during locking in timeout situation on windows under python 2. 0.18.0 - 2017-09-02 =================== diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index 071757a1f..d634e4931 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -77,7 +77,7 @@ def run_hook( color: bool, ) -> Tuple[int, bytes]: # TODO: Some rare commands need to be run using `conda run` but mostly we - # can run them withot which is much quicker and produces a better + # can run them without which is much quicker and produces a better # output. # cmd = ('conda', 'run', '-p', env_dir) + hook.cmd with in_env(hook.prefix, hook.language_version): diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 1fc920725..4f6136ede 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -160,7 +160,7 @@ def test_xargs_concurrency(): assert ret == 0 pids = stdout.splitlines() assert len(pids) == 5 - # It would take 0.5*5=2.5 seconds ot run all of these in serial, so if it + # It would take 0.5*5=2.5 seconds to run all of these in serial, so if it # takes less, they must have run concurrently. assert elapsed < 2.5 From 36653586a0810b4dd57b3853820c8c5f741554c3 Mon Sep 17 00:00:00 2001 From: Thomas Romera Date: Tue, 22 Sep 2020 23:02:05 -0700 Subject: [PATCH 416/967] update rbenv / ruby-build --- pre_commit/make_archives.py | 4 ++-- pre_commit/resources/rbenv.tar.gz | Bin 31781 -> 34224 bytes pre_commit/resources/ruby-build.tar.gz | Bin 62567 -> 72807 bytes 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index c31bcd714..d320b830d 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -15,8 +15,8 @@ REPOS = ( - ('rbenv', 'git://github.com/rbenv/rbenv', 'a3fa9b7'), - ('ruby-build', 'git://github.com/rbenv/ruby-build', '1a902f3'), + ('rbenv', 'git://github.com/rbenv/rbenv', '0843745'), + ('ruby-build', 'git://github.com/rbenv/ruby-build', '258455e'), ( 'ruby-download', 'git://github.com/garnieretienne/rvm-download', diff --git a/pre_commit/resources/rbenv.tar.gz b/pre_commit/resources/rbenv.tar.gz index 5307b19d63ef650032717a5ebbf667198809dfee..97ac469a77bbd421f59a4d234ffce3e4c47194bd 100644 GIT binary patch literal 34224 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 z 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 M+Dxp-i343@S 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 literal 31781 zcmV(`K-0e;iwFp@MYml7|8R0;Ut@1=ZE18ba%FRGb#h~6b1!mYWo~vZbYXG;?7iz+ z<3`pnn!l~9s2DN^V%eDE3~72o1D)m_8u|e`oqWL9vMoTtwj4=@CN%H!{n*d5Z{Yj0 z|L$wqSMpxTUWckul_UdwqDjWgq-{%eT(xS|I#<;4`xmuuzUbd9{4LMU;%9w%(fU+> z-_++97Z;Zn=9ZS~-^|wM>h<|=#Nsy(@HdPTFB0OL-@>#0ZRPHIuO^~C|0I7At$)=! z4Nm)EArKL47jhEDC&hX%)6?9Z_OlgPUW;_6ke`xW;8 z+;W!xTbP?)9Pj^!_=|^~a7CPW-Pm`43?yE+yCUE;5bbW*_l4_r{6TUi>f$U(2JuR* z2Jg;>t!g{$)#5nu2WNgi4*Rv1m&ACAIB`1)2FZ$ey>{?&wN#}SwN}tynZze0k@#^^ zi-vut>-A5E-l@ML+7960iX((8_MUI<{M^`CJJ|Ypv$6Th!D{i-`k1VI-u-_O^rH5c zl>ct!{}*!n{~SCT@BfF`|9&DagKmepzwcMosb8C^&}m_}{{COz-Pw4veo*ao zzQX=rT3BAn?*GN5@&131zX`D(_LC@R4U;f}?-zdG>nHG~ zue*Nd)OVb%zDUk|5%d#3f&(3ko`k>-L_z;lczqEL{62hk#HHT_Rso*^o&{0Z@A>_t z0*{3kJMFaVm`e9NXc7JcA|wXXl}JK(dX5bYyeJ9U!N5y^Ktb06#1b)x!r%OMg0&s| z4W0Fd{UEs#o;dR&FOIQ8_ayRt;lCTe0R4X3hrvd|2U>RpOio2a{;3yrx_%tvM1Uu$ zh*sFSa^m0*_}U3#uN8Em&5H1ovp$Rgzn=R2jvtAj1MO3PFn;I(z<~s`BD&Ci7u!p_ ztvG!eyvL2f;~0MkZ!qv-tXM)s9{uZggHvh-1{(YChEU4v$qBK=gj%cW)!9$r70e`d zoXvM$4RiJ|IG2mq+k zg?YByz@u3+|;2rLmoK@)R&ey;^fV*_#6=izv*uAf4*b(Nwe&c}q-0dzl%X&dnh0ycFt zlyG5#9(;)yb%uS0tFM;ZOA=gfm{=G&UgyG-06K`mZWpFefzIM#7vWO_L<~rP<1+#<0x2%*azP(J+#>|6 zhUK^wSuuhYS;0b3sgh;W$TvUhPS;TU@XZ5rrtSFQz- z9az!;8g7TfZU?Z*!>P0YF?hA9sSm?wHaN1GiGm?94N#z_l1fqmq|4NXQhPW^iE33uxftNmwItotDh+?MG zBl3nlM1Bv>03dXoAVJ~--BUv;TH&{VzjSb5Jl=T+J@v5u6$zD`baWo$cS5c8fszp^gm&pG9Gd;43Oh6{jTG?2m{89{3gDa!BUvn-Hf;IK%d<4;~i{viy@z9jB4Eh&v#05a`F5ex(BUX37pmuI2qghX^s;Oq=9>2TtPX=K^$4D&e+!;S+)8D@dKop@MO zlA$X)FBK8OdHDb}I&5OZdzc3vX??~7LLNu*6d6P)!}9wc9Aj|C4j4Jr1YV^RKxSee zfWNQRZQ|)uL;3dODQ;O*$vt}pMPCJR*SmlQA`n@E|L^~X!aaIa?eU{_rov_voI|bl zndf(7cNfNq?IW{xZk5F0ttFbCkD!I{$m@6mT$z(7?9p@&i-e5h!!uxa2EYOxSA^6% zhO(DQz(z6NjjIT&dLzD(|*s6V)&}RfdXMy|!?CD1RI~PTKm4xT6V2SG%*Q z*;~!|tH}S`TkD%U`+`c?`TrsQ*2BRS^Mh0EX)y<&&yX4*shU5> zy$!_72dZ`o;wo(MiXh1vwm(ph2nA=!rLy25!WPmnrMh67Co<8@{pcsoOAR$g^U)Cs1QzsoCnI%?3s8?FHK%GPsv{^V|o(IREUUviF zV7Uj)50fwq;#!5qSrNSu1Xld?X<7r8gep!ah^6?9Vh=KN04AS!-Wtj3a1=uefv;)3 z8Xfb+Nl5%F%RMok7|<`AP(6Dl03u*;`p_jGv~YgKA@#_Iap+)iKF*|s3_LiVKr2X> z0j{kOWo5m*aQeYec?h@wsnn%j#b+Kd8%pFSe$_Lk6Jhtr8zjUq!eLG)US^tbj2*n( z6#Kg`4&JWqZHldZ@p^Cf=dF#+4N+R#hu@`&c)NA*a`(-FfD(IaI|sjt-4|kQ=U4Hs zt(}dEv-!*Gz0Li7vAZX>UcKJl+T5s!t)2DlHyc|!KZ)m1Z)f*FY;V2VI)FwGb_I4Q zo7&ohdM}(;n|tdo;pf`(t?jLYUn}Cp*1-<8`2reU6R+3y4z||cY_IK!*KhV-@9u9x z_Z!a6?#|ZEi#_ON^VR0gK^1z1uEgfg@I&msT-)BJuGZeb==W%R>$|Ui-P`)<<$-v) zyS=dq51(&3+goeTw>P;fnAH0A+SaRz*jRhD_R}WS+J#p31QwH{d;4;e9zlO=@c;FL zt=%05V6wiubFc?LD=^Ew16}p)*8XNitnF>>BS2p4?LzYiPN=aPB2EvyxR zBKZ5w{$@Isjm@=fXloxUn}w@m_5Xi>{(o*9V(A(KXn)P14uBTj&4#3O zVYO_K!~|QAvq>_iNrZ#AD&A^LrP(RefYL2goC5#C)5^HAFd5{3QKMOVgT@_F?*hnO zRd*WNfn_Vh>h;llB)n5pe)N52(wgl~lUKYAvlhRFEo7S3&-@eMNZY{x)hy5gFpNW( z1U7Vm>i(`&4QtgbtTe)s^)Durp)%ko8lL*?D_E%uKPE#MtmU8!fcT&P{eRMp{D0!m zLUn`JE-n&-I6!42V0whQ z4p>gVD>Xmd2v)%1LtnZ%v4vWXI6$=}nFW>vlGfq8aK;$|AmgvRukVR1;Xa^dNnxJQL(qwRxK)e8Yj4yA*p zqz~{4@$-I2{FYxMC z(y;ai%~hnUI(!*k!ZI`cIi8w03oqRuagkut4uTTa_99;~C^xkB9NOp#OYftjYOXlB zugdc4I7_8GCj|Ie>UX9l+VY(X?_64ci5#--cG&h3tL_%N#nDPb16^4lZ6GiTOym4B zHSG7)%geCmw<7-%SGDK0clQZQ{LBseE<6iqHavgY#kBnpp;TprfqLWO8&@?UdwK4~ zL0kM3Brk_8Btz|Ukba?73!l5Z(r#NmAJrRF&vyiM^-L}$IsstMksQJ9q1^|fYqu+x zpRt=txLCr)5-ydH?MRp@lA*V33P0@JT(A^A*}CX>Tu=`mYm5p;_c!d{)?>CdO)Xao z9}+IC%L8}Mr^CAKBM$O2H&V}Jy9`G}FoYY!jDA_~PrG5uD8N6h5(@HMz=SftH|8qI zjy*rCRJM>+Ci2g`_)Hh!pH>Mn;pzhPEAw6gMHOIp!28Vt<^hLn5L;J2ry3h6j|Sxo zj*W?xNtPUEEkPF%IB(fgqoR&9WGc|aBN;lmRaPx_)Vl|%}Qzq zh@~OhI?4f3qUFP(D{UQYEy(iC(v0uLF|=}}JX~l%UN`KYI?8avrep5HBsgSMm=Jk_ z>o{-Kkq`U^@+_J)1O~8)c;7_!r5SnWKFo$sR5W8xh=b`T)-rhiFP|7)LeQdE~lGKj{nn<2W0}prAqSkN8pv4Qcu4`X|Js5K~tH z{s?*@u?BUK1{jEAOvlK^YQPORyNThK3N{)vIuFDS@JwyLL!L|_v7vpoc_Skzs{@k= zyjGu;eLkCsE5t!G?b$Vru9dkloksvjL)BoO3Jgs&n$rXPQHWr4Mmvt&;<(`i*W=!!CCmko5JCQXe$1#gK2oD&3XrAB5?Fap+y)NZ4;K`do)B@aW!dZkLa&N(B z(2eN}Zad!efG@Ot36_LjT|}fypJW7+2GQ;zmimfFg+(Y*I>7?G*bfbPJ{@c&6#;-A z0J?ft6>IZ5+0D=B(qRImRng@8v`+|Rl0%Zi3${OQ`i|@e9lCdXC3U)I~I_CXdx* zv^WWYy+@kCq(}kZq8cm?3640SMPOGeTnOc!Nc11dqTdXVdmM~;rl6pWt`i;;WeJEV z3^n;-8x-L?41E4$2d*H2v{3eBQ2Y-aXl|6)o-t6px@^Uw;)SD?=i#lRBVu9V2hrPR zhC@~O0YF+{ATeVqnwn%=T^EcIZC*Gi;q(+!N7B2M7;rGI{7BXJ1a# zR=`$JqLruR?NX-W?BdZb#VXsQgvp3jGxEWbSGPhfNALCu(v@4NUCKeC+fYZVZ8Ci1 zrk5pPL2Mj$!~i4uqS_@z9}?4@92UgQs(68ZIEppFHR#dNrW_SvA#@O`?uL$+d~3CO z_3zbXcQC(PuQf-N_(ydfO3W>fD)Afbka~5wij6pu$kQ1@EUZKtWc2v3MY!=>$)j~F z*06BaB#PLpJu`!9EBGP(FacBjnVFST$<_$^ZCRr0H+VKDr96JL3TqRUbj*ezK)W;To!gH2-GrP7x?@x0Cza|pV1X1*;of+MT0Md%6AKSjJ^q!j+i3G4|z7hp>~I!#g?ke3 zZ;N~P6?vDVx)Se2%ae_h3J&LXLUJ{hDkB~s=em*+I%_G)TGdj%I8LkX{>v`dw0Qe6 z5QYJ|YZUjP<{_q1tVd6v<=Mr#dDgM5qjxXyr<&SZ*uDZJt51s!e+TFygj5obKcqGG z#z>o>lc9hPm#eZR#UW@qW;H~5ia1=c1sO3{ovSX`;J|u6`2a8(2oP(Ctqsv@#+sPa zx9cLb?-r4z@z8P+B;SgyEXk4{Giakz5G1nEhn*s+pGh|k)mUSU_b8wqw<$eS_Q+mk zJXw$kgD76CE(Up68-&!M-$m17Vi?SZgAQ6f3{0RlV2^k4(4ga{<7A8~x>0CR9{~fe zeeMArfITMnm!!$6wA3K#o*aTSog;;cYsW!;!DF+Cvd`%AgBb`V(Hv%W=>6%&MCLb; zLO#NfcRcZF0A7E{QCm?6EKx;y6>~(0KZvC>w{(ytDf!fA3FS1@>tev)VCT{^5Kj=a zYO(hmbEB7NdzEA3b8wyF^tJ9XO!rf>=uk;R`)>yb#`0@ri*^2p-2un2b+0NSVZBIO zTHQKVzzxC5A;kmEF2Ps|mUDp@#T`FTN{EdRr~@?zG6(8RPKXIXHOLPd2Dn2K{1@dC z*i2CmdrgV9qSzfhQt`LH3Cur2?&zn;4#5NDj)c38FVdO7alF>)7}SSUVOoVdX(%n) zq&kv2hBsXk8&Ya~vZaH_iM$)be1=%tnHhuwFmg=Ja2@nf0K@j%XQ3$j?~pRfbP1C$ zcVBH91CmJ-%Hr8GoFdk4U}O{KSrH}>gxTwx7EtS<*zn`?BpiI^{v`~Hd;VXU>HhI4 zz5S8v%NFjZWA1Lu-TfK4yQQjVOsQjSx}jxD+do99j{%`Cf)1-v3fQnzQL44_sDz14 zPZA8*Gx$IpB1%#B<^ik4oE|VtTHwC$jae%bN1yjBZ^551!c#L%7GLOO_!5k!8}c|w zJabDdzr|5BiD#S5>G@oB+xp*$3$LgcFl~OT#htg z_I`d-bPmNxKxN-ZB1ogi z7E^O-k^lQJh&X62r7A3YoDBO6UPYHvMQle74OaWi>&qBQayL@~hi!e!u}iGw*iu>) zDWRGlxzBCM0#S1df1U=p?_^Dk2d3__F zsjSlbk|B!JXUO-e(@%*o7%dO5KqfV24IZI4yW0h{;=FPwn5B#5buYauatc+5!#2&P z-sa2z=+$787-q8=kZWxu7o-&=8b)eCDzqXVaw&95r}CllMnk5bSO>$~tgE=Y8o^1!#Kxq%)anm6^cUBgHY?{ApbbllD4BeUKldq~79*b7&IAk?jtLNrGY!br)xJteYV4gLlE+Bc z0@Q!)rE7(oT)0i`DjoaX6Sl0O0Sh)iI#ME0IK$i^{aBu+NSc9ucBHy>l+P4L0F64A z6f`mB)U*WK8cTwc!~7c_cSH{IPLnEzAP$o* zhjCNf5`YB*&XqEOfa=6&%F;qc#w1J+pb7Slz(YF?9@YGdGnwHcTsUwh^e@|eJzsjR zI$ND}2XhO{e4ed!ffeji9DDal%Y|7ur-;xAFZ+0yaELmK%B(`sCo<*Eh=(o9zFAH$d4$(uD}woz;3_}$pQBr+-8&rKtuG+|40X#>yq z11>7lT4?~m+zAe{foe<)jC8=JnUf|dy>6@#DSoENkxv@Kay$)f+>9bh5@Kb_QwxaA zRGk;>%Obt6RZ<9$Xd+b3Pjz(vc9aK? z8V0jyroq)f!)ApO7_sS$C??Py+Ddc+Ip)L>>_ph4`h@M3uyvZvCb4d(loNw1$jAwN zhh`}}Y&xi^VX}zHB4=iDkQE1413twnxwrtG!XUNaao9l=BtBNAnw zf=^9W{23<0t>F{Oc3Z1fs|-7Ki3&y%=(U(xhtrTkKPnlZ8I+U(=(XF!-moh(+^}C% z-nf`AWmbN%m zeM;O$t!nVjpqhU5v}(U}lx=r$xmo42Eqe&D-@ca2Jbln=Bd8&lg^uOjPcz~SDCFf| zuz1oJh&U4x=r9FXs;y<-4bM9-<^#bO3I~XnbZ;`NS>k2OeGL%g@CnGic-&D(GlU$0g78RRTAkMQ+vqtv#;98 zFd33r_(E<$=J9AKNn4Yt2u~ZBQ?st(#SC>Y9&eEEtALXfpek9DR-N#cai%#0pR7yc{ zfio0uRV3OiEiQF@0t1R4CM0}Nhg{rz87NoTVW!V?11+l?1+BYNcqDu1lrO1!s8570&}$ zyJNB3IE>RA3IvtdSbN7E_B%bOz6sVk+aZ9*;R{jo>-51Q!X zX!H>V`q)caHJ4whDtW)tuhDz_7Qi}%d&xE}*~_iBwNtpjns1aRP<|5TnlT4pIJ7<-=7*&C%uILW9^`=(??Ha2_h9O-Wh&d)$P(x6V}QB{(+)^ucsS3a!krV= zrha#p&25_EXd_axD2xnZ%exd+5Yh(#T}frJT-VZ*(YsQdnVhYY-CcU&l>$gZKw3$y z@~hT*r8t?26x%_Jc$u@Lpx(T%-4|tS!C8383Hm5$Z+s5$e5cdOr2E3-Y?Q@#q`T&` z+n@pJ!9S{VAh7;pRtD;&bRVT;6r)A$4Juk|vDeE`y=mEqIc>?wwBEsQWd=e%)pxn? zx!HQXIy+wg9t_%v2VK;d7EUn3hLT?_9c)ILQ-MKS;U%d+JhCTYfX=8l4$w z36z#;OKQ1GaY`A$s5zyxBuiRmQP69ZEE?)0G(trTlQJ>RC};rphY>4|ieMwU7ho)V zG4CORKcW_gc#T>O%TX9XhBd6AEB3Q^yJ>#N8cWJu=SD?gToJI#!$AghoT3d-i0+~= zSkiGc;U$4boM|f@$MX2baE)*iWk+?GDSeMQHa$!(iQiZaj(G0k-+Nd6rQhH$NA2Fj z|MsOrdYn%mk#bDcr>*GO8Xgp+NV_d2MRl^byL*5=q^?i!V}tU7#u?oNOHcV&oLG`Y z+-s4KsfF!vSaOr>c4PDTo1b`QyTfEKOwdukH9R$mM%s!Ouc%h=RuwGwG!-tM=~iFv z?*6O6-be4nVFNqI;QI!qOAOEBV|cobp^9!Azo?=m*}WwV-qWT(s#(?J-r6H*O}>7+ zL69;-+}K?t<+#Y1p@4ssxm1miTju=aa#6$*r3gj^Dh!=cbpq0`0pKoAk7=1FqTThq zJ|$yAO;W|-NEueLr1AeUg+-uU%_+aIJetuA& z644ay%+bKU ze~$cLc+7pdKIs0}#l;V~|8;43aeV*l!>&Jl`*Q2Qy!-*{5C1NGBj&1Aeb?X@8{K&Q zGyX7-VgD~Iz{hz1f0gx5-Kria|MTq9T=xDaC_T>q{2+e~TJEW7aSe>$`tGaOdz-Jf ze_fqI^$V-PFT<#_TAIAZmyNf(dmHy@3p%P$E9s!()hT;eKf-8OZNw@@ zDs(t|oO@&p^w?2kxGzLQ3YcW^`uh7H zBO)GX|JP^d7xVjndA$E0;!nE+HyRWKL#yAQWQq;B9Wk&Rwobd(5mWLJxfHnJpgpZN z{}f#SBPOBLwA}lyM?Vki$6{5KWjw^O)L~HqmRU-M*>UuPx`RKR;z5zH{t1IRO^_f_ z|MYg!i%u_ibTrPnWBJw^pBiD&IMe-mBHXrAeYky59u3Sgf~B0LcV&kzm+FzZF#FOG zVpK<^(Ey@3m9V!XnnkIApyeTV2t2o2Kb|hp1jpj>e~A2F=nwxu`M+mp7Zw+?^nV^$ z!ZH1SfIp;!hvHk<{haww+QW`b#*K-d13U{+ymrzR?!Rn?#j*HEgGV#z4be}p0~BnH zUmC3A^#nb+Va9RJVjNArtbkWExx-OKHOW&f++4+-JHDaHI4>O8pnF9e<;Gl6YIJ(L^1e1t$re7SsT;sgp*Tf)-xyFRYfo z7l)JA&)4=}HuiVl?5%H3Ow817Rwl2vw{|vnc5jYFS@QhuJ8Il(K~;I+T&rVbg3pgx z)rV+%P=VQaI3qX{1jRvRS$a4^Y{)Yzo&f3A&&zp_SUk(8B;{AI;fDvgsSs{`n!Bepmo@Ef+g_h8VhkuB znXK-TS(f8vV-8NEZ!-ih0{f#dQ`rX*t zE9e%UDkv+I1bJJ(6RWGDHUms1&OB42mKMY5($v!@=2prhm5fN0SuBN>y7#D=Hd8JY zjZ}8FyS+g_vjBjSj)O6X1E46ez!9LArQ-nYY^dcm5D%J~#obEx&yZ=nfw@M4erC~X z(r#WWk%yKai`~;=bQ4X2tlh%}tHB}_Pba?V&&Ok)8$3QN%BzKM198x-9Sg!k2H&SHy zNn2H4v42n*jO!xih&&mrb;KvPFQn0&T+sW?&FDa_^W7CER-mkftNN8j*r9>2$~T`$ zUK<@$NL`K3N*GpJDv^@jP~nk>`+IOek8D z_ccb+BmIrpxAMq{zbcalmFa3+EopwlZn=-XL3H0e&kjhI(i$3!3X7}6WQlqzKogfH zp_!dt!Tj9Pj4k8~lQ$C7%#p5jRLYGxvsy+E=}>iXDx6(}(&Ud){HB%mW3N5dI6eaT zFWsjPBmdRw%Q^p#h5A_k_toUT39&zfjT&982S7!oBS{~eX$cVn{S2=-` zFQjoFFGWR6=#VzN2wZN>W{LolXI=gRe`yNbk!hUfkHw&Rq@&G zQy2`{M1mxSUr@RcpLx%q6bFenjrTf1P&xtm2CwjIr+E#U$i+YtiFbNR9^F!8E|hjD zCn3wN%AtO>RIk>nb0r6dqXxeUL>BojmM^l>rp5U}i#+WfhQ+`sXtBj8pbdokI|E+R zc)PF3DQtn=xm(bN3v-FZOOU3rJhnbfGYwpuzb7tOW^l|?#4fVpPRi*+(J|$Hy!2nG z)*nd!S1+*tEXaS#;i$m9q&)&OkKZMWY`%}mX@tUKF zM9z4VRY)qUxTR4<@`GKc-)RI=vzIDQ(q>{yAc)TlC8poal%=-J;{DL7fX#2c*k4`Y zSd7&v3RRsh)EivOt7$39OsSOExkzi9$Spue5UD~^>;)d)R7+{0?pl7Gn3&;mS__pb zWaG1}c$1pqWc8KVmH8W*H;N0n(y0zocL(KkSCX-3sUhtb;dS4hzx^CtYk}pCO0r6a zGhNaL3b!#zX74d2!SBw05dBwM?m_7P?9yC@{x8?(=g0K_>(T!#N!RA{rz%`c9D31d z9rI?)RjbtxpzrIuuU@U~Y_RSr)z8Ss=3Y0H3$8oF5m`gA%r!`N?{RYIUPhlEsaiuH zaoD)5sZ?-98Yta3@XYzwKx=df;HF`;QdwiB*t1eXI`aG?Wf;zY+4^8pl>R++IPu*v zoUZR{wP{s7KkZUFWg2)%6d0oT;LDR!s&%r>r|&ULLv`h06AV`np)VT-@ByS;%@UTG5kEs?-C zSv~@ccRp~8*652u7=H+tlj`;*UPU?=7D4ph%Xo)q<^9FpOZ&D*!f>yT+YS+%`Vo*=l`QP z|MB)4Bvohf!Wx-b?DivH7SdK2ZZeZJ&q@HTQ-tX>?gi=fyabxTWYd&7*T}JETqmAO ziYBxD8kzmd& ztE+b8C5$Q!kC7;^%Is4n3w6w12cFIhld5m6q&L-v)ywj@ZN5V1-*MfR7x{&pMZIJ{ zYP{Im-dxRi-^hkadAC!2G-lZg#j0T96_+wnXX{W;rCdwOtF53Jif|}y<^6jL439#V zE~5X)bv>Z`w}ir8R{onG`+q+U`ma0yh!tm|qCzlpWS`0$N`C!VNIsFuz4oHm=QKwh z6PKEj5K;Tu~phbYaHMPS~d8y_jv4t`VdhRx-62 zw}?c%1;qaxjrM%62Bz?As%e(-Kxp!0%IXH9D$mZon+?t=Xd@or#pOVY1E6%2a+>n& zQ{F(#4LEGYnAr(>mG3Ayc83P3Xn*AQynxb?NlylApmc{3LkbmxF<+fWL%HNCv7fH2GLP?$PWY0k2o_;Rn&6qIxGLq~nOFrb|@~kH%yUE&ws5z>ns0mdvrk^$C(=F=>XqBS*yQnf~ zSgRwupWtLX*di?8sK9#LVP-X=9tNA;Q`1 zUVlWZqeiI86YW^#53?m%T}P1naoZdCQvjT4-R3DkYpQnGJUW^z9oH&QAO8MUsv!3@ zZAXB^lfq_!P3zf>Qk(xi>d#<uh6bD;7DYhXj(Q?; z1Ld!Vaiw7gy`*fNDr;MYZzTibsZ|fjOTli{nK@|A4GF>9h!{Uq8<7`EdN2EUsRYa> z2Li)ab~;U;!e)7H$etJt=0ud>>v!%yV*H2^)-0St;KyAJMyv} zTs4Lh`?A2$!9!S01`ZM8856VsMxDrlx4|%9qrmrBzRS5f(@Trf(M|+6hP$)biyVKh z6T3eum3^CWou-oecu8`_PTCvDJJB@rbLA)1lGKnIsVZ!Qpea7UBL0zC92>$g98So? z%tVIy$So05s}hYb8(#PaSCkmQjjT$mYmR%dSJ5y^JoTc}_}THO!M~$BeB~0UCF%L{ zs=}=A6W$YnEoHW#asM5`SZX&^Kmn&!HZH%QtjSt6kR})=1-48>oE?*xbI8}$@b z9D^e3~B7s~%=wRk}L@BCcW|7UR-9*yPyN8B>MxF^G zzrjL0Uz<@fW5=_R<7NanvZ%KPk&g%yKhzQ7t7#?Dg%pIG-#emQ#gi-dSD3&}jmtS^ zZ267hy^I*zP%g}wn9@RT=8P+ITHmy1XKmhCpqAM=wHeAiszGHWKTuY1IVJihAKp5) z>3-_@j{@`q>i-v(XBTt&|FQk|QJnwl>Rv44z9!Q!s;`(xPk$vOnipN8QJ^2Xd=ejn zv_&mF!#slztGu?NLTtvMWqik^it77$`t=Vxj)VjXjN0pm4Q>m65Q7B5B$K!w(P<}* z(#97&$O0A5Dyv_WLoNq;=PITnx0^@@k!wb|4FSxvY)dZEU6q;|#CUwedG~q?)A@#? zjAK|Wj|>k~HLvN*ci#yvNwVTBo)j>N&sSMd2PWqznuEl)Cn zpg9ql*Fa8?*g9NdF>Oec#AM0bDEKmJOQfTec-gm&K@_42F)x17hOa~OT zPq`wPy_gW&$T9L3wcOe1f`9-gzg8 zcz;<=45e%Gv#cm(51N&-GuE?Z@DY7HeaG&xWxF|QtSpY$i!@h-i_*>$vI9rw+>kRi z{RZvU%iO&79W3kiEuTzke${B6Q6?qdxTSGx-xQ=Fl|5P?@N#!P1 zDbIYN9^ezYsaTYbOf44msnyc);mk2xVr*Q;c)GNj?1xcbwNaiaJ4QUAgheS=OXftz z|8PEp{^xg0lY}Xh42{XAwP!MQ^C?AhRT!7#87bk74zLLDba~11_C2P$QB4~v$)m~Z z`U)RP+|ChYkvmP&EOG_MGB5Fwl{QCY;Lo&!X~xlH@e06;A4}6Ern%F10G@qdrg|MQ-=T<4vGxT4__s&6JQxO3U^7yY=4wbOtQ zkzTD$SFGXBPUcd(GP@YjJ-F&ZN$J5Uw_nA)f3k}zQC5X!+JQ!Hy{}vF6?Q1TduP1z-&z?ST0+xinY}5)Go;2yN^e#NW60& zBN+X*-=Ui<(ScXqj6Ok=wVin5u@CL1`d`rw`E-}s5%D8zfyF*7Ep`v(Aq&+VGp_%` zJ#DSFMtayn53)#ufBrPB5nrpUKy3O!sT@i0y5sr94rg*(~|FpKi zqVpdwxenevT>Qs!KK`R#AK(A<81#S2`7rxV&79 zWqMgMpNYm<4sXf$s4t}F9?Pj)v0@n#nXJspGG<10rnj0u#vq84w$?``-L~ktBFBH+ zMvm_nv}t6?$)67S;6f<6_vZPpg@Kkxi#nvxHQ3<(^T8-_O+A(XREmsmg_}l+810O-eyTwp zHM1?2Y2bRt6j(eZobsJH#q^)`9uG$Umlv0F_kYbUkLmwop#OWU0#kReV+i;Zibll! zcqfS;Da=EbJB{Cur|(Zx_g`+kD%?cWU&(pBw|lUA@ayZ%hE-DkPkCNQx>T z6eb}#SK!n*ItC9((xD?SY|SaINcU||TzV0X3!A}P-aCF2u`G&&LWaup;RyJ{kLVWo zWC)@Stx+iSI320ye}*h~eqBn=jhK7(UYG_VM_$h0URq!s?R+8~Js6@ZqNpD^xzXHv zwvDtVd(9$H5l?jjVs-5UQBR!p_vWktXaM3%AO4U03dy~p2wRC4pqc<3%t0#mPhn6# za!5(!4X^}VUoVCO$%ACp4qZ6anzk`PIt~JWPPH+&=Y`+URT@-f^Ovmy^iC;wCl#eE zsEt}3N6O3vSVT!OlzRrnNJT!Sf&q3c0iNjuP{|`78%%3~o?z0Oo^iH+g(_MXikV?DE83ZU8$FNb9pCq7P%lA=|8V+Fmln0E_pUE6zB{zXLN#fpW z`FdX{S`ZGC4j^lXa5T1zCip}-U&&FKgt(R_kt7F1oZ%?MMjH#9u|^s(AoqW(TgagZ zklBN2sE7wTNHK?vNZ^bg>x@8qnO434$-FYx@ zy>8$k{diBeWwP%c-&Gf>cC4J7O2s>x?N_Oei2zuMI+fUhZ5YmfR^~}XrxjEPD5x}6 zG?r*FV?(p&oilBUo2=|?;bEhWBS~T@gBFq$SP zMQVz=yZtt^-^Rol5@+6pPvkC@SS!d_Aw+Oxd?2VDLT;p}Xi6xe%o&PW6>I$~#w=o7 zD5atgI|uWHNFtny0Iea1e(pK zfXLs}p-8OS2a>ib_!s0FwqzpvT&pNXOYUWcEin>+!(?)S>IOv~!59^vM9#>_=Y=JM zM2N|$LV~U`@VY|~C@}>9ItghqWs)Fjruu=IxmZ5P<>va7z@)g6j}qQu#OcNMqjh<^eq&qki8H=f?TJ9;g2wVa9!Z{&@S0 z^!el0?uf@(S3vK!zVFE9h!RYZ_gc_ruP|y^s;TuoXEGCIoO-hrxgc5`P(EqpU(el5 z`$;xkx-d;@qEPTbjhPhD?cr6V;OSok;V>pAjKpy2kXkFe;u&~LRQJCne?^G{%}T+V zHb<@LDIyEitH=fwQkcb)P#R^>)P*Vun7wZ(aGM4$Hc1)w=Wz7XH_V|+$cLSvsr!h^ zrD^+k+~r$RkUGQOuz6Ry0v8M0pjWnp2-RuXbncKo84|8~=*51F1@S{h^pK1lqVSlZ z?JLvR-QE~Ajx?yo7a2FkTtF3fk!yPOwx&5%&Zw59B_(^kl?90VgCLEOfOcq3zvlsA z<`p=a%)N%*Se=_4HNI@*s+4r$G$={FDsJ8=U${yIbs9z`zhF=%;@!XH(T%F;7;e+r z7x`ubRz?!4@_sn+uwoiplR+YDk<>C==NkV0-A#TAk6bd-M=J-P)#9lad!vn9vjVgq zy;cQSpIfTC8ywjdQ;tttAZWl^pX^qg5U)_L9qRyU?Pg!-?1-yry5O3-cvNZ+qlj@k zdm(kcckJW`(!b)cLbZdB^a#@=fg{2z?@JX6;|vw{-O`_2p6s&IT9z!Ix&LtePtbe7 z{7 zK6ghbv|q&QnwM>|xjN&JX2$5sR1DInfbych?4zzD2dE6GM6Hvj?RGDYY=!NnJ zi0N{N7R5k_DJ%rT&A#QiPEY?uY+1MLe+rs=i1@F$x%~aF%VYiTSKR;B(QZXr9zN{^ z5e9V`HexuE+ezwxPA)jH3f@+P!)oi^I~3OD)K41Fr;J&}^nVS~$2QD3PRk8P<_V&t zeC*1Nol|2aOt*w%O>En?ZQFJxwz1=6V%yHdwkNjjOl*69^PXRDZu-8j*6ONys$Xgz zTZ6Ei3V{FMq#BRcn~l3Ea_<#vL7p*|Q#W$W(DTTc`7nvAqwA)!m*O0j_)z{TRVtm- zT^ONMvYJP@rSTlcss4I&-ZUg9mdML{*?;ty_Q$g-=YVZ)!JL;O7?tGCeNn7~+_8K& z3#tg#?lxPm(R1Sbzngq7JOh0szq7MzR!3j@1SQ{RSss<{E}DGN?xt!C&JYTuaWCl_ zRcXU_x(r8|$MnNhsGhoJZ5OyvZLUb+dCymn4;W@eyM?4-Jnp)}5}3P+M{8 z6hV_$2?w?Himq3zPB0sXvM2>KY;-!xd?K1gxiU9l;AY`uZmWj|I1QwG7K^ul=mF2A zUx2ix4?uv)KR5L6_sLIjN|109;7)VFEn(l~SHSJh+P$nj^-2cEk5{ul625JeyNnr% zjHd5mPtQ1|{+8{P=Xu>J+}4P$ zI3)HBaI_ZK@lpny>qd`aL@#+SlyCARZ)hJC$YHf+xPC&4#%6vQIq6>fVAS>O6USdb z9Gz8gZ%-QKcIc#*SA?C4$jLmYt>*%6@kxt8g zhI_`wDl=VTt%df`g#qX|JgB1u-o^BFe*-%57J&?m{}g`je+vJ@c0shytKKw7eb}7J zRd2aSq0#8hijp25kz5RpijWY`OlfKI;Z*Y0f$7U+!YIRuh1N6Ly*%rANgw#NjHJ=;LS2NLNmFVm4zie1u|25 z3js^PKlP*J;eMuXbcn?y5zbYULAk+BZ# z&5QHG)Ntbs+yglVZ`7^tT9l%2;#|CTh=cnV6_@b7omI1AG#uB~Qesu6W6 zSx1e1taVFb@7$vOA@h#;rcrp@0_CH@^rnPr}u0EZVyN`VZ^J zqtDrA^Ulhio{rQH5k=?JIchczV%apxEng;jRwuhVukJ+G6igg5e-+$cuzY8H4wkiY zPR7ZSfi8>2FR6`v+hI;iYFNTOKUzw8)AM6GUtFa93pLH28euU4EpI7V!ii5AVxpSZ zv8z4CzOovg!Y^Zo5fYO(e%yke2|XJ88*ZE0L#~NQv5C3ClBXEO<>E`niQ~7o`Khhl z*43b{XgvdW6H3KibH`9!2mY0L>x<J!ao%g5=dX>uB;hz5a-_us8{Vy1Pqg`BRr>U1FEfvW#* zp?@-;7)dwF(n^|io%WfIV4VGKm0OKYPVVU=L(4p_j53ofv({2@N;@EFOgx9g;pa{OKqCGp~08}k&QeG9-E@|4u zt9K20erk3NFATx@Y5U>(J$eWJOyxst#5CDtZ$H3%f89+HJWT`l?-H@}{z3L@@3T<7 zXhNI8s=Q8`d137sBq8c9sg&C8Am9Z-8nfSK|JV$E7vgo+(bPLV_dC)SISP*ELDu1) zaImUmbmEh(!{;h9B;a83I%Ro8THGRVfrcC1aRObVhPY1O@Y*p{ z-J-45@)$EHtbDKGN=PE7ZyNZ4d}Bgt(iKDg^Mj%BOoEI4ho?g1MkUUwoROn?j(fsR z4sRR(GZK*XlZYzgY^hhC$a5W-Xwdl9_ZC+AU*}5((0A{NiewL^8aOz3dGGbrxA30- zE|57^``oeufK?leT?JOv3E}eWS#9mbOfFYEHC*&H*E8RL@0NDRf4*$#`+Hvk?}!0o zTOsX>Pd+tq(4U=Nb=qsE0?dXCK=tEanaFzO{_=_8ul-=WJ4Sk(-iF3t%fr46G()4k zSS_Fa;UA;<2w^V&vsW?}-bN_H~TCn`P7n)Dr zm@gOwwZ%~lYtr`<^$ke$lJv&$GVgic)AoyWOli0DU|I};spiLGRj)2 zzM9r9Lwqfj+HM_`$C8s#>c*Y=c{Rh-I8&r4;?s{@fT932A)#bTV`C{4ckF`fw=>2F z8_Wkm!U&M3g|uAEqOZqlAIfn1-xCZ`I)eDMz+NBL9;W z7VoZkJ`fodvVaWS_1b_V?HsM*8w`9#`t$81WsvSpsR2b?qc2}Pi0EQ;nfrAJi_UcB zXB>)x1e~qgVV`M9axr=cv!Xt%tO(z$A$P{Jtw+o{iI$_m=zJANZYu;`G4YR|Ixu)% zg1FqX@~5~7)N4hMb0IR^&p*D|UFu6KLNDTo?WpZO@dlkX0=OtPp~+}7U_SJlo)Pr%&FIq; z5rNg$@biY!$76Eu*Qwo2ZJj;W!pZRsQEu!s)+4%*hRzfHR~W(tCT02UJiA{R4Uy=zRKn zU}FI9kN_C|rnR~3H0;gFQ)(Hg#y%>qrfIazxzo<~R-EAnr%~0>)uCYZfpG-qrKK$5r*$CXvCmzQC?fi5gt zy-mkwPKj-=Rj;PL=f$!Ny-(kbjR!8hO=vy7*y-Gslg(^A<(&a)0(_+$@gYvCFKaj9 z>tng}S>sB}+jS)L{e;NmT2EZy-d5y)={BdF%Q~R)7Sy&)ko^(i%;T3QY-@S8vcZ^U zTiY{IjAp~;Yc&a3JTBExKv$ytS(2$GF5IM{If>aV^%qc>u{aa+4}3t8CIuUp2HKas zbNhBZVB<8@-bXC{Rp5>24D0dd8qtWLB7`LS{tMs0FE$_Gg&YoH7O;+q!RiZ$nKts4 zPxKQQ-wT`styX3UZo;3i;I^-+v#Q0MyK}$tP-*lbSy|QEHxL9lRh6(YR=C={55ZPF z#b2^%Ta4s+w>EgctGUjE)|lgZIczZwc_p|n#(}7iTF$M3sm*eyOTNoj6N9UBIkbiY z-@YxHESR0ou&SVgKU`LgAUrRcZ|<(VkU097ec@s)Rob(ryFQA(LHrEp^@36Q5p6Z$ z9`b#g>$^de_8QI{k@CtMv{0fyrHXJWe!+$1+2yoKF{SS|lUOsS!F-w{mUo3ljVCW} zSXZ|zKejuCc*yo%mZvue+Wl{Gu#zM9^L;j7RC1j$JsCEAp?Mjx0A{-v{j>V6Sc$<- zUpDd9Ju{(>z`K^1I!yb})5sEu?coB}Z%VZbI@qFH8|fxs1927I&Pl&pDB=xWGdm&N=C_?nWC=8WU7`d(|p{ccRonJbrde_Q15iG(^)8JeW_=oeMEQ2{kUi_SuB9!vLsv92fsjrWbLt&^F!`A7vCi z$JRD$Vs29d^VEDbUnSP!gem3~mJ@Nw;>rcj21L^I#Cf`du6A{lIV|QxeYo$dh{dHV zgxtI>l;N`+V?3!ZRf(~A5|6Z60UC zHVaN$X6@HR0q9Z4iuT-cW_?la|XuFJHSBn@i8E2S?Gt}gpFwzykY26kh z2nFDWE$D3y%w5Mo^7YjQI2l^ZYTp ziK+?6u3Ja^#JSEDu$Gcz(MT}D2GD62_w+Pw}BxXJQX$_{6-ZzA3o1g!k7YzAp<1D6a@!0(ZBGVVwn4y(BhDiq&gjgd#khFMJ zb{Wo!K-PA190E3^j;hhGH3=hLwnD1~gx*QN~l&?cV0~>i9SouD<%D z{ny%ZRda>^`5uxm!#X&M&2C3`iqM%sfoX~9{dELKDXh@Fv84xqbzjMIw09)l z-8XnYv1vKm{Ue6d;R6)ErJiOpFv+uyxaFX*_m`)NDF|A4n|m-Adp!;FRJoN5t9Iow zBNI~ zDD#$MB&?R}cdL0Di5@7vTMZp939Hkp{yr}MYns8TvM~M?YxUu1*x$rW&v(X#5w0m2 z5~bpxyS%x!Gn4Aw@@&-Qt33G60r@~{l&Vtjx4t;CTO@J7a{U-vB@%0p#g3Y}`=?D0 z3Mep_LrGW*Bxc3&(s1XsUo0w7SA0Pig2poe_LM!B`1zeTJ7dmyj3}EY3*t!ijs_VA z$)+)~GXqG?B4LnAs!)WOi+Lg-V<5C#7*`u7@m7`b+wWOCUBwo!Mo%2Nt|*0HK6jcQ z{tPm`FzlY-e}euTGq#askgt>J-&o(x4$}2#QI*6Y*=qBOa0*eXOPccqT};Hkyj7+%0j=kJU?LQQWx18P=;2EVU6(9k{Ad!#zst6Rk07-Hq7B9c z`U`#zIKI7EVRM7?WosMZk-#&ubxJt9he-jl8v+6$ufd-VNgY!sAH`L%e8sMu+P5K2 zP$?IjADxl`rlZxBOZjUqd9@M0U~@JlQM!B7&kCIMet0}8k3VxytuY!C2RUGNT|tik zA(XF}`PjbgAWwr5K(EGj55MQ7#J_ched>-fnDSQ537yl&_wl03BnUYIs5BIi^Vz;! zaaJ;PtO2hrN_%aczhc{xzJ44RN=s|nj(bYl;l1rA1fXE5F}_k|HWgi7{~D*^hMMzk z`9$G1F%#+h)-h-30-`dM1q$7eyJ3FJHjgJR$8Mp*qPLd+;T5e~anbts&e5CHKcaWNL0@ zinMTkCSL}SKEy6d==Nf_%?lZNQdV< zZ{GO^qQ`lOjy-vtRNeeJe*VD`;oTES$UMvTVjxoFgw9f1 z$tmqfPyU9ljRz~7# zPemeDjVT9{CI9#s)lpR#gA&X$EVAL?fj;V^pN1bG{V1+6+_Xl9pDTd_ag}M9PD*O| zd}>0rGBe0vfVf$?5XTvl5#4W*t=JGH8lyoSWio?8Bb=#F6IN4r4>j`L&b>7lxN^4J z0mm7l4JULggC}si2`k<9&)awi)jFuZW+c)=$kUch9SWe;I_5Gufn$zE+~zMbLDvi6 z6qB{O)lsYXnFEy%o(~>Kys-X@YuH|zm|7t=9ipZHk6erFJdj^IttikitgrZO5aMwV zX=Rqck7@GSuP?9&n_Z>58SWk+<^Cw{8F?$1O{I$;>lO;9=v?|F<=I>LOIV_+}&>qS=| z6Gs~6grs#x72AQK&X#8C$?)JBnu(dQj0y3vs^+c=ezHo4sGyD8(5)Xg@^3SN^K4Wu zoBi5l8k&oq@SPS&-aC14A?ftjo}wncRE}@+fG=A`cjqL$i5(gV4ZCLEO*%S@XA3$-$HSK zZ049FB~;o@kY&9ngBX`jt@fuy?;tA^nu99AD>F>H5oHFzle-&y`aqh0yqJJsY*-lr z(ILZZNx|myhm|%mWMHpm1sp-D!k@j^ zAN8ycqgVeO>^Fq$0!kGht)g=xK~Mv_yg2YaE$H&S&AO}_G^~mdrw*ck2GOS&X#<|5 zRW&mtorFZg9)2hDTOABh$SXn~!!S24FwrMjF;t3Vw|boy_TV{_=*tU49T?_m=)Z`x zbrQ7ur1a@f-r4Zxy)PpFVWlFvCtrS(5WL*(B@g4d3W%^X3vtUm3MZCjD`nzWXYy)O z7RV7xl%WOLAKxeato=PKnpPT(DYh4lDx^3TKpF6324FYz9;zYy&esg`*P-0lLN_b- z=aU%xP;am!2>4n+XEV!5J{-xsPfKH8o6Zbj_kBVbQ$92&l)6jAZQu7Nn}qG!z@);R zsi&{+%hk$^lBvC;TK787+LqATmM~cSBRwLh%6xn5w@6X~>eq!^qL~Pnvk>pyMh1W_ z64_X8la<{?zTWx7iq`jVNoAp90L{ zrxiiGQohVyxF?F+pFaM+%_8mZ^4!zU(O33R%6a#TiY(?YT4$VY_6?94Z!^r=GT@^bqIO6; zsB-Tk?UyH-#3b__%Ej#;8LcA04<{I&AQuw{EHtsu3mEtWBb55w0uvu7e#^<1`L6JE$Y|mR9v`7SubU1!C;9`>XXxq)uMllKalcupmbw;&bEH-U z%4?DK?GubE;3PGj5Gv#ISLVX{F4o^^8>7F{ATaSN-wg$Rei%kEkL)JoxUu^D=SXbz zTPBU=`&;n>4E6LS9&h>02H%IH3y3FGu2|7+4h9ZUm18hoXh+z9UhGP(!-8FsrsDyz5%Yp3$2h*7tv#LtQGo$WTnDWm;pWk)wR=iRyU zJo6I6S9}QhhdyFw8=Gk*(jx&Py!KyX*PG?{4ckxqc8`J(&a;*w=};;v5x)ND78pZf zcX$78m0P;5FE*vkJh1NcIB{XxZj3zj#7eYBG6Ia7(7bI5pB;tKlDi0 zVwBQJyzbw}pWjNc+)WGZvJI9_o<0K9F-)}}J2jXeL9}~!19O!uyZ8>;;x1I~ z^uo$R|Mb32Bp zjKJTd%&7x_`N927#X+C@6448bpD2}iDt1$E-MO!bMSUbzgDcBUTiY4yndT(TH=p=Q zVtSQxmf3)pbiGBBs~}Bd9vz;Nk~H(NDlLt|9lAJ722L8lZyoCsC;H3Za5T zpe?DNpjB_p=0M)Twv2j}JcL>)0Xc%sl&n!b19yNYQ@6|=B>+bM4r59Oc_h0ARL;gd z00bZ3yUyc&+f2$GL`(u%wDR?)c7W~e-RyC|Z`!x~q%~*exU8jEgl^y2_=G5w&6mX$ z5i>JerXGL9>E+j>GED&lR9XBKcgwfCzkGu9%pGkmGFtOLtwtJ>$VHzf>M*oGiwvQ)z|N#O=OhN z1(yG6HLFtf-trO*V9l${X?Ckt4Qw`<{HWP#Y5KpiH&wtu%U>vRbPL;Gp0tc(Lbne} zXysU&4~MXR6H`-mFKyG-t)0p>Slt>z*E~iXLN+tGu1ZwpUKLo^{n5&*2YO<|lhZA+ z)@X~=gG`t>p<+!ILVrk-+Hb@5SzMAlYxlWp&ba`C4@Ntu*nsOp;LN9ytK8cI!1`a# zlqDCKL{Ecm5|yk9{nQY*&A?vj-~0GGAfVy zdqiB^V=3^=V!onA@P`zGl*$~*oUY4qQ3=jne+XpkF}!31mqJ`dar2&H%qMPKAcUUlBUuOOYW{qbhLt zXA)@M#3`LJ=~N^aZM}TdhN|`$f>Q-&60s;N`a#Mwe&-Zr?yVK4v$BqPx=$Ct?Gk90axF{XTyliEf@=FS~&Pr~eE9 zeSv(S^Au1>^gZ8jhBh8VRtQ48>w7jksRgvA2{hG&iv#1@NG-l}`fnQAp>6^JT5{@J zpKBxQ)_Ktt#h`^~+`7}>fqOC|7jM-5(byO*4=#U|+R+w3^gq0&(ff99E_#+Qzzmsl z>i6g$fm0)|?E*7fkYxg&;}T6d*JYjTdg5D%oeKJ`=oCBS!weKP1IkKu0V#!_1u;;D zk6-dPYq^7$pL5~M*%ncnG`!_ET+l%XNdX9p@@b7(o7J4|p-CEf0#D~cAR`(%rG?BB zgCNNqThL&08w|)9eqSENmhK$6JSk^^2~8ssrI+F(kV7xpz4!-1%!N|CjpheSD#P8b zyNuxp3F+L0{e1UJ1Db-j1p_nq_}a(v4!$T>4ip##s~CDr&#;6QNPntiYKAgvlS(Oy zLgF0&p#gjPWxIFhOY=wQR=H`Sd-E!+{8tl0cc&FMCa4PwkL-FJGtuTVF5frOF_5K?qQdf0F0<~sNkQeQ5Z#e0*vq!dV&VZ4R|B6V{GT3%xk ze(;^O=It5rLl|I%rq1MitQXk)Q6Vq3`QBHq@Thl;qHDPjzNtr9=T0+Qtq~J|U*Hz2 zOvQ#GqFRwRrrHp}%+2}-ev$B7qP%W2vK*=!frWGs}M_b!#6;Q0Lpl-LYyBNfy~EM7HNZ{YRpg_tk&E z+dn(R>myL@8yMd#^PbC^<*G(}avNm|&soyMOc6HHYQK#Ndn zn6w(@p}Q2cl-}RORZV5t#J!9Akt|C)Nc{sHfvM`pEGoZoq*-$_EyJ@)GHpr?cu2o! zO=2jOBMjpOh(jjU4&V_K|QYQWC^DIFygoMP# z;5LbDZTqWhe%sx8h0H#(IKn0=iSTvAVCzMYjSeV{la
W5fBrVj->Z19Y$^|0ZG!>A$#Kr%{68~w~NF;0|P>qiL;%bMzmP~zvW`ORL2(r zt}@&EgiHRo?UX38yOhcH9D>FsT*mEQCzDrZvsWRgAe_3Z5nE1S_hQLknA`rkExL;N z7I0`?9$1zDT1hKQKP>l!5dRWJzx-&RSpEl78L)@q+qXfGD}50-6Z_N`&+;7sL;E`* z#86&G@sU7K*jP9ZVSbaE2x}7s$4J;F@X*YWp*(9ge1Wt!33@M*A%0xxdim_ipiCXd zbqfZkB4yv{1~+<1o$lXKIX7JPNB=xOk%bo#I{3gq9_#12D>kJtZ21NO%bVYovC$$! zcMmKzr(qap`=xw3oTQ9{gePLEqZ9hyaofv_@NsZ_$n63Nbd+A);-n>1J;u%X1z5^x z&~cm|R35Jg%KG?P?W#~$D#CZi0A_jyCL#w$-x$otnND~Q%Qr6}Db1nB+LBRJ@3H-X z0)j7mAXsd&^$XY4?JD~yb zbBKJm5?Ok$EdR+%h}T*cBOcjdm4vbeW#1$#`0d0IN7rDgV{UR-y^)8_ z%fPA5GqkhP?%nAGB(%IveR}@i=*uS>)ZIKhgN?kLfv)=9=N~5-wg%x2f}a~(R&Z@K zD-lyia*U9ZV3C%!iG~cl=TtjgD0(k~feeQO!deH`bdG_me3}Zpm2M4ia-5#t z_`JJb+5iNaEdXPsFSLyDamDu?1Ni3QQZ}PXKJA3M1uA!*F52+8JnF49DWp$*t1c*0 z=M*n^4O}C-q0eS8O8+!Be(Nlk*qMzI#l(3|!`n3f06;T&{J~sE@$|ArDn%tl(By6Y zh#@v8_KNguN>R=XxgY@qY!Qbn3Xb_UEOiwMUJi{E#VYBW)$(2xxyu}bCt7ma#YYQu-nOXo7ol?Q?xp*W<*!d| z0j_QxynX^wzKtSDOJ}E`{KjBi9M4C@zJQxujmMl0Mf%JyAR* z^;9^WiUP11k8`|)K&Nw0@KcbE1pg?4G1d9iPQ+Vk;g;_r~ph%TQ z9SX$OXP+?c6>HhCR&5I04a|4f243lq;bQbU-XZJ$lp-STvHaTQsBCcS|2-5&caR?l zIUVM76nuQWzJ&6t(*%B}sp*6XV0VWyB@FhbhZEjN90s}08xZBpxGRRlR>Ply!MSh0 z4jktA?%4$7eodwAn$@@ z7t%QaQ}^C7_Ji5G7p;})OCCX=Rovgh0Z71nDU_h(Q=X<+e5*I=w}J%N5k!sxsHM8I z1}wp4Z|B?@)<|3pQ5h?rA*yTCr%A`J>2#~lBDXENzj|HTokIolk$n`6%AGw}SZ~13 zbOtP1RFGBOiVct@TXtgj=z3^9grdiNRG#XUgfy;66m1Sa31X`yBx87ICzDh-lJ*$D z7v?_(6^ayx+hj)(28+N=4JD*edTVqOwTU9eZ{3^rZ(^_eXq?7w3?{OBU@g7yZ zEUHWp6e_AEC&AU6c@wZ`*c{aoK(;9#+`DEcFHoyGFC`0B=VFE$oCj635|3$t&#YBKmtB*CJn{V3Qsr0!KH5 zGN`(af_S(iIB7_$?ujsvCbqnPkL#jXWTwLZ14i=B#HYSs7J$M1G z0M6$$tA?U}g7NzM9~HuJ=GGKqofkP#S9NVIt3|rbry~wLW9q_`HvRM1{e1T}afa-I zwKDgZR`176Z(Cq~T!J)&DQgn{ol^>j#V1Qq)U$Y0kP>^Hcc&BC=8@xT z*kS(Jzlm!=DMRLyz|DJ{JdhKU{I-Y7oB+y+hXZQ`t;|}EVV0lRonanwX6M2J{&{*% zg1leuK1~`TOzz2=uYc7^MD*@}jWb8O2$W3E1=C?(PBO_{jZ0%bAds(PSy?f9M zG03?qbAw}sm*CAa5(O>0d6R2;N?{{reX{CI!l^GCRO6D*PSAx;YLUoDZZw8FtDcT? zFT~8FU(#v((%?t#g9}pjAjb~!m441qK$2=J2xOql<}p)W2YN7k^ohOG04eBE%0>vrJg;D^w$GW<@X7F!C9+jsimnV62a9{QZ20EY=piHo-Rc?U5J*q+}f5vp#`Fy)|Mulb{^XP8lxz+prg#@ z$#y3`4ZGEw)e;1o=f!EDwgUHZFR(c}Ry#h&{qCMf$nWV+@p?qiX2QFXUxk|bQ;|N) zj`#Mt6jY60vnOvTQvxI>;xW#q@n_d0zgg#aHnCY7 zUIhmJ?4*-t_K08)W@*5tJF&e!4;+Qidqe5? zQhm)|Mk(qY*+%oG$qLwI{Fw6U)`=MH@_B7q`lD4=`o|D<-2~-zukViMm}T7O;L+A^ zIy6E@ifh|1A)d?3_x$7?>v~{E`VlW?`Mr$bIyeIys+nP+Cm?u`}THS1<8_QHvo3TzMPGJs+06 zY{iVvyEjDz72oiH3=@GU)4m|^y{4j$;b0nbpFcG)kH4O?;t^pZQ=uDN!jm_bP9U7} zA!yk*ptArtmnn17&qL6*-G;1}Var}*U?>TJL{-WD>u>yB zt!3VVwO~)K(}kduWOk41$4w6&1LcjkAYqxn#`do?#3e$|%ba-{|GO#xMfeeYE|Vo1 zFEI^l=jovJev0(1l)7eR6DTaxp7xuaaEurJ1wNK)N+6n|_0IAZ_3R_7uq)|CbFgr& z?j3T)y&OeCT(BF+ieipvSi3uN>UXcSB(KbNq@S}Q@cHrYA=?e7h;n}YK~xw8&Xl!@ z|GQAro_SBy6?2sZy7fwNk-o^-bsS5ic(`jEJ@TZ5vOh%@HC{YJ$ux?0(E&seG6ZYQ zKRpgkD%NVBbK-_1k_RgK&A#p9SW&N72W_9?LXS6HC2EFpSjxCypg3^SLSQ4zGcpSV z0#@^OA8^aHqDorUC}2~cw(98FbyakM@!1=$U^$&ZKl18~_+w2`#7~X{FA8@cAvyD3 zlw56MloFzq@xC#GZ{oh5L^4WS5p(mjHZtz_dWkW~r+qX^-Gc$aCZt?nKgUeof*s}J z+k?S872gqsnYif4qRBI3v3P+N_z0L1)f*VHmbtMLrlSN4f8jkAa!fCA^4n^2_ar2> zg*P$Eb*Nlv3(9oHpeSssNxz`S14rqBU>BR5C5?d*D~t~?2o9@d}G6W6@C*8d7REsS%)tJzh*F-LPl zS8MUzVDtML4U{*0_{t4JTBmD~@~XXu>fL~+e6i^0XMsYHChP|#W-LLgF^nDjKAy3w zWnW4njd@#qqp2)xiW!nJ4%@(#G?B9CpT6P1nOCI_R`dq4J;!n;xbq<;>LNh7Jok6z z`?#VJxJ!tX(O@y(85W48y`dX5&4h|>7ovOEbKVCCv2QPNLq{DB%bb4xK{Fim$MFdu zfB*&j79HOr{9*x8YriiiC*#?uJYoRoTlLtNJpu^HO@y>L*bie#%P6&K4X+rA9AMOx z_!G!PCub4BcghatkkUeDa(yR((QYbQ4MG)|YEYBbP%S%_o6HY>>4(4^mtB6342gPi zetAiMel5ht;&QH101_hLdN>e!TArrT5e+5EDGX@_6`lnNduG5{tmIhUMB*A*GnDpP z_HlXIU z>cGA!k3nf>Wsw{;Tzq(g3ENF6Bsn1QXDvO@0Rz>js}S~`Eb-;p6ebwaZ8`r_Wr>>g zEk-WxK}(18YTg!G<11iW7bcdy3mgDvHb@3hb_e<{wLAk49om6kXqP~akmH{jcw2Zf z4$nQNT~s!H^9u&J5mp-NDjz>wp!)hKtUj09R3tFSvj*=`*I5G*8Kcrk)PEwgiZ#%% z^)W%0+0$gyM57*pH!v=PWfb!;1a{a&MNyK%f?1g&&KokZb;5;Eia6+UOy#f#Tl5@C z+U;rgAF^tufF;KUU>FVI22z4k^^mR~&l# z@hi2JGz4Lcs}=%d-`WP>DCd zheh|8+-UK`F?RpIY8U1}J!U>)&}}B`uTr&zO>04Bo zke%XGU-NsS%Reu4KzAhjH>Oiyo6PrNQ#KOHjYQzDui?;EcN(M=nVfPmhs8VZb>xL} zC(q$Ui^g)Fsnxscw_(0*VyGK$cpUU=)Q-OwGh&|)7%+1LuEfMA6q|`w>j@6CNB3x? zaX&8_3R<~IH<{Wc4a2BatcMbbon&X+XlLG5HElC%J3Y)8wr^1DX>q4t;A;n+G;$Qp zP-VA6I*Sr(f~bA%?rn?fP7jAE#tVZ+Tn;5{I;Rg|y43fHxUfN9!_4pp6h`k4BmeEN zsyt>Oe6=+bptJM#8j2}*tG>~i=`4rzvm(|ysEb8iyH(W%?7t=S=#x}0r1JvDX$&eJ z8!7`=l90yf!8geeM0L@lxrF&8K@DI7(|*r9u%hI%UlHNK%NQaFV7|pObl9eVR9yR3)YANLgiV_JMns~rI!M{&cbyggiF67u7Cja+ue}w6T?R#!|7=L-`vrgY(25_fe| z44ZW*x(6V-GEKHxo+{SA3^mNt@&>htS64scm%h!ukeM+qfOq2!E#LY01ffqt)j+?m z*^}tWAFZ@T{f&h@MFW!$QFEmZdT6gnnc0y;c!C4UXONowg09!19i|Eq5SebZ?HqsO zhR;~k5R>orFqd>tfY3?_s|A>Gh1 zL%q2rEknEOOZ?B1zbx)#A|d5o3B$}$44Ff?-S4N~`EwJn3so(>j6Xc@cCzgJ*`4h9S+gH#wVH0MEiSA+ZjG36>*`|rKvxo=8 z^PZSF;aIIh6o`%@2BMHQZ-awZ)~Vd74hFwpkN`2#p5$A+;~7h4-D>hV^m){zm_~9e zmY4%>K>qfh&-Hg*iqJoIZX2q8*O_$#Zo1D;O)h!ZXg01=07kLhz-SOMBOeSy5rmb+cG9sHYn;ZMNX=5N=jf4?0$r03)(m*02%@AIBCG$gaa!sasvWVj5&mYR4 zTp|vk&vYjQ@ykNKRfuMbq+4TaRAAm%&YUPi|8&Y4GYxeJ1B;JzF(dsJu{51JA-(*k zckxtiuWY&_M5APU)i4iqc0CA3I_>L%nXZ$rQDnmG3&fn%vzEwGR~uPKh4|X0LfpN~ ziLa+f{~4B(XtwEfkX`QAIh!vZgpB6bUWVPniHu?`x@{;`k3-t_`IENLiVZ^n9}e54 zG>E!dj)SCes-JVqykvSvO@1JR;!(crYdIT)j(qWiz}MbQ8HEd8$*^#8Na|Mo2XueNKgdaYA1fd4G|Upn`qi)p|7r|kb$qwxXs zziBpG1^qARe?k8X`d@rLGyT8zgUi9tzYfcvaQ}Z0{jXN4)p|ky3;JKs|APJ(pTCU$ z2ky|dLbr4~8vfC((X7|!(*FkM4GR6Q0s{VxRX*qb{|}}AV{7EDu>-!)nm=CC7@JJP z3;fGiWSxg=Z0YLxlBWIU_d|So;g8&Y;9m0*y#9;nXu@8L7i{1Mqv_D1_rY`=8UU6} z`_pkWHHKE?h7r9ABX<&VD2y3w8oJQd$P0(oRk;rU4cU?*02-DZdH(o^E3;G=)fn>z zYfS&j_Q#Rsjl*S*!==C-tguXEWGq4Y!bKvox`~9H56Wpe=*r{gAXd%8gUVK ze5ID(+?Ei=OKfKM_|C(m4;QycFQ|?<)SnTxw*iXU#3;tj5|APM`|BnLe$_xLfw*TS$ zztx;M{{#Fh`2Xklgj2^~V*_i5+CORI2J^!k`aAT-(;E%6>c|=n*BFS0%pUqc&vJDKa@%hv`2MyfT#(v}tJljIOpL*wxEN{qlaDUtjr?%~e;TjveS8kvQ_56J3 z_bpM#U>k2X{;{{)JFN9yyxshq1QcnS^sJB zd+_US?Y4a7HrpHj*nabFu>14Yi^kQ(@9*C1yfuf>%kkmvjngvEk2YWZQr+v?;f)gv z#;?A96HXh|tM<$2zgok0<$mX8+^(I!`Zahp9(CWm+c=0Eb1<-&6w=T|q^N)W2aacWn*&O`iMfl%0_UnW3)xqH}_V3+wt>FI!|1bD|@yX%;K-;># zy_dbsz1^+1n@6RQ^LhAxv);<;|E+q#|3A-Xfo=NZDDe8z$PeKAl{>b^5qxo6<`3|t zGqod4TUcOw(}3*{UFY1@wCypAE?nk~BR2q^jYS7OF#f0haeQ- zQ((?s;EzY{I9i3r%nCI-X*#6Rz}(4>@51j1HN_$yo?aq=i4{bi?M+A+fTo!b9hwCG zZ>}9-Z4G}zW25QVi*A|4F08-`Lu}9(1g^{6n+bH#9osHi`WcE0w;;}}vcNsJ0%r)4 z1P%m5k5$(9om(yR-oaPL3$4C41emMLjV{K}2mE^Oj)7w`&jI+<9`qktfawUKf2(W= z@P`O5X?9f`Q|B!X3?7I0gISY_3w^~BEU@Tbcj%oH80cu|-b|sC+LGoY#E77k;?+5f z;1!G{)U@u6HG&zW0rlMr5bOa^bYX!ACnp_V!sOb43s7*?*u)RL$h&gYr2_HK9!?#1 zP1ByUw;>ixmK29f??bF5er5ET&*!V#2y>DyE46kxN;&{~AI)dh>A#EeR zf`Cm7p@Z27{#Eqi9f`NQ)CKx9vT#Czi zV>~S*1YBhxhhM;kMjX1h4XF*>U_)4az$4EFiP>@-+#0KN=;lu&T=pSuCG6wKqfG-X z?uoS^vI9%%0btvo4jsTI3y0DN#OS+(5wIZw7cmg_PvAqxc)u8)Ji>zk--*zd(2a1K z;(c-eh>7)SS9>t4+P)woDgm+rmr$5W4mr)RhgRSXZgD~oyF$6)h;GFAp~d)tYk+VN z$L$C@3p*~dE?uT}84C?84~HEDE=O{H)Cr@}L>Gt>TH{+r`WWKET&yhRj|X15)a$@I zzaWa443Eegwh*`@-~mAB94|uR1I-hl1g-F!$G^kUK6uv$ZW6(c^#hoBSbOLaB8)YL-XM(_-!g@b&?51s zt}N~vs9|gIb_J#oaiVRd%7;Gv8Nt$9i2D9? z%-0Mt7h&U&abl3ryH3(Oi{p1h(8fT?h!g@``U*^Q9F<^)CZ&-A4wM0nETjP1U>eXm zJMO?kJi-a)+tY(zNSJ5$0AI;2k}lgN8V8b$vYo8{lrKJ~f*d6TLS1{X2lKgr=n)Lj z4a08-$ETI{I`Wm1VB0dxzyJ#-41j|ur5bZ)Bt6pGFM`A`w#KL5sBiDkx z6KXynFmIYfGd>U;?|=&^B;3#@5OS4-Ev6@+iAJ2asX|G68LuRO8qz|b;DCl75Jlp( zA_ivTHskY6s0rN&U0}h`q2cWu!gqb)Gsh>QGXUP{UlViUfT?HM?F{2N_kBkLq70+J z)&>?96=dj^_@yL5I4&1vjhIb{cn{-1mNw>?K**yBnIeM-OR)T8cmWJ#jeYyAVrKi{9JeeQ-@f|}ioPKi8UPUZAgum>{|5?lubR?`Ke96wx`B5I zwd@Pa9frmp^b_Huuy&a$iQU_XVtPJ@7Q!RTu_m}OgTNoraF6qpjMHOu@R@)Jbh^qw z7RUKxW~siaSy#{~Z80Dd$7o|*E0+~2Z`cAv$q&xUqH5VJ*H4edk5f%cps%qTeIzry}Ebjmy1o88^R?w_;&AF%&bEA>`p|2LYoR$>499G^{p za?AC>WqXBHD`wSze`*TRUQCT6CFZ2h|HR^*TjfSLBpxog2AcV%k|G+~9XBp)P{ZQ1xynYq1KK|Pb!k`=ZVn-n zps=B#?E^JBm?oDL-!d8OKpqDOXj%{gpX?o1j-l&h^@zk2ktGhq)KRJ|&NOH_V`!2~ z+m5e=KDEdtaxPbMnO2y|KoRFhr5eiaR^La1S-i%0oNDvwrUgh$FY;A*VR4N^Xo{q? zTBMZVaL61v?W2Mak=6(jJ&C=UVS+4s^t#Iq_g)_Tym8QF+lOrbVDG2xt?m}nHxA*q zzRG^yK6<_P_J~1=gN@yzU)kPEwz2yw`*C}BYn63>*+1wW9FmLqwUSNI~xaV|Lwv4-eDJ--vY3^+q*9hpq1{M?(R_uT7_q<`xE?Nhp#tw zcCaaJ<1O_50Q<)__x681*nai;h`rw1+3Lc>7hUMt#*3XUZwdyrxwEnTW|eJiyxDlw zrCNIcg0Uy=l(m|1g_{{Qr5*|CKN|E%{DnJUTc4VbLJd z{4^4$M0lJsvMxay1Oe8yLOM7SZi4K}3! z=qe*_Vcq66EN;U*J39+6v;}dt9mg<2pGTIxcc?8g`Yng2&ej6kphFJc5#DVmKe}|h zfEg2}FD~;}f$v8vdYP99p-AFNnd3l7b;?OY=s-CSR(ZBRIphAA7NHKv-!Hd+Suan+ zpgi>PP`9KMDvK#8!Sul-;?%+{PM|%9&RXTf{^VKlw946>P8>XR`1{}GPpsuWWpZ&G z#^w{I1C%w;T13ZSbe zv}&nZs<0erqCiO1L^0T2|5GmURg#b&UT$R`&{=l?>NnTZKCbHtG~S{A;rPI(TMXra zu4fDLWl^9yK5S|{pRq4o`;xYPcpFCU2sHc&`q_->+^6rt8y8AnkB2^?8TrDXr^JXm zil9S_KbioYCK^E<5!42Km#h$-Tf%9;PsZ3WZE5%SsTX>Ys|~$Ao-(`P=`r^YM&d&U z*2(k$VYo#{Bi_Ff$EgkOp5Z8Xw!k)lwE(euK_lEs)&R)f0Aa9eQ0%!Cf?@) zq2>Sn@BbGKDw81|c+s&dH7+|dp5ieUjQVZJXZRTETX^o6g$8q|{@^4T*9jxQX@zMT zd3Q9WXen>{Ht~4zd>UBt;K>yY0?Ol&raTg~MLD#WTm=5~{32r_QCyqY6|6O`DOyY3 zv!BGTnLYWQy_}A@JI0^&c@OOyb3SbTt55Zz_Ro&q>~C!!oaK!FXSDYz$DiCHhx-DS z2+JUo}U{jql<_+-N z-oL~ihbd08GTZ*^qoe)a=FWC^_vj4T-{T$xXNaLy`)rk++0$TnMxxHy^|>3JVdv1- z4EL2n9@QQVyZ{|F$5?2}ol$UrD|zGJIN08(_V)IV4r%-w4mT^JTi1m15wu`hA^nxz znr-}}*MIZv!H#VH@%Cvl{y^)q?f?9$d-U}7`3ii>_TOwD9PAzRU>0cp1u5jwDh!^I z*i-tn^ha&~@W<_a(Fw%A7q@(@3hv9!knfrsq5B_jt<-jikN-g$_5%+v9p(=@AC?~P zz`@?$(OE)(#01Q6taw+awqAGvBtj7dlYRc=3D^S3cXu02r-x^01VBzo%O1K`0K>** zpW%JbolPG^GDG^NI3#MVJly`~=2`wrwlCN>IjcwsC-_qkVDsh9#;e1#boAbYs!~XdoCj_9Ou`^?@cD7Rbu=Y3ic3*D4dVA1~w_aZRrE<*V<4$}U2J~j* z$L?9~{0sa$L#;Q~G?S5^ozshz(jRYM486>M@gM2&^FDm4fzPzRy?c1Hv9qHt!EF19 z*8Pe4q2S*v5`r{#)BNWO1Da(?yuPj17{-x&s4>z3OetHp}F#mtFAMb_G zy#Y|jyBR1Bh%KP2yg--MtgQvH|4bpUQW@cf8N--h?K8G~W=w$i4u%$KX4u7B&0_Rc zk~8Y(a{0gTZyEjp5;_gbN5OP37`pgyMT~;{jim}m%_5i8L`Q|Uwk4d-&JK^Zws+6g z5)I#yBLj^Y{tR8-jT|+fch4+!N;P9ruXfnCtc+B@9QnjlO1xqkCa*+oqww}Lu-`7D z=)#Qfa7X%`oqJKqQ`=cdH8etp|B39pi?$>i)WU&xW0pi0yql`{`u@>A#D*JCw@~vA zrz7^08^j^Ev8o#Tjw5r0QYeFBj87O!F%+Ix8%=x#E9(zsqv#!=DEjEBs##MYrWS=F z3`w<=TI%8JjVd<*U`H@?24v?vahqd-3{A#~foan;IR=gmd~0C117NFgDCN>O=;{~f z7%qfmE9K&ydjwhHr0`j9Rd*3 zNj$tTHBlr}0b1usRF;&zF1Y_IEYrrwdIy8q9Qsq|C8)O`8$uJX_|WS}%3&U~pbQsu zX5eb7l>j3UMAwy#<8l}CH96Zo*(F^Vbeg4x&U9&3q=y*fLf(=|ky3R2ZsA*8;qUYr z`wuxVa|I;|hrT=FUWg zxRAs;890O!USivk#`{KI5zrCthLPBxRQ!qEU1Mn%4PQKtW3Z+fslwM*C_Iu;855Yu z7xo)bR4g;PZ$pdnX)J+zAq9*~C($bNO3+!^?WHgo4qDd6OK7i$`9u(7U+hoS?oO)h z(4LZQMxBmtFy6wLuNwdq7h`f<2XOt@jNvIZHi>~CL*kJ*^OT(uH|5&44+#FJ@H)=? zG7OQT0tEZJJR^!|7?;r28L_`r9?GOeF4{+xaly=Esnp7%#e%cE*$$ykJ}$g9#)tz@ z`7yfA<3xJwayKC~1e~2aebLpCJHlwFGfuCNrNUesDm+yN8C>EJD1HEy7NFfjWo{yL z@k*>3!WeUg+i9%vaFTe$)X)QGG$n?p5?)YTmqLKFc)tjDqA=A<33QI3NR3*QrakH& z%j=%Hr2JgymB7u{?r@@M!de`RR+!SFe>@#nE#3bpc`&*dVE^aUO=~|+#n@c4sHD@E+h4D z=$|W!=>WDll*hhkOrEK&J#Y|&|6)nifV=jTgQGHHNqJ;pByc&ah9g9pw5Nf>qF5!d z#6P{>LO4AK4hP)Ph^xoA!LoDuG`Kvt?cdEQqVWGI{C^7npThqK2pp*1Wh2h?K zJWt0VUUu=Jpa_^^;Si@vYjUP8%iMO7%oLi@qi=1${r1i|~{9HS!%m-AUk$ zBXPjaEm6?*Xvz=#Bgda|?21t-82~1iQhoL^TjQlK^IK3zU7S0KQIend7(?A-AsMQ) zg_Lt&631wfO97pdaVA-G~{kcFc@1)5ZF1 zbzi)FrL$A?!Bu+4hlEBtJhH;oWTW7jq$iod-hHr5C z#+W15zVL;+a{3q2spJWfqU2|aj3ke7+b6&H(P#WqY^Br#qOkDb#clCE=$Gs}!jH0=Z z8SnU%owE1up=abzZ|d5;raGPqaGY}gK6QiJbt=W?dsDw>i_6jN#6@ra^YZ{mBK`B8 zUEoQyVXiRaB{W6AbZk2crY<(?p-*!pS`1#25r}w%*d>c*Q`XVo;qeC!b`UQ*t{)Ktg*$U-Ard41S`u@!~SY}9N5Z7dx%<74| zpYFshN{BYfSElcw`$C!n%WIp`9GH2C_(_yFd7}ai9+&hKKy*}I%JbyM&9Fpt9Vz!L z&lYg*nfcw{tMnBSM;Yr*JXQpiJ6;v#b07=|0_{Utofdta&?G4rQn}OymE}{2k*#-0 z^>7Ev;(~tPWjXb|*c;jB>v35sCAqP#G;&3a2udK^5eT`i8&_)C36&)xB1^`|3S}oL zxO^RCxmkLW&>B(Yc?vB}CfAK#x}jxra;zc!yj$2fc=Z6G>BDb64d(120MgJ z(%ap5(?!a4L`SW7UTp>El&gTrE6Dg!UgD#e#733aHpyk^&>?GiA)^R<%rqvBz9pJs zK1<9|D)177I76nz0tiynpo389+klY(i0UOSj_`qaGIu7D01sXI31Auq9!1A+N?O98 z5Cf8_s=!o9As{IukSA4%$LAQl?+QJZRB&ZJoYed;@X!gDopFJ#mv%xT2{U;Xa}ORa zVWHbUN)lYcxBzh>~p{hEK#k;o8vvC@0yk#LyF^I3CVC&0LO@|sRxL!r%)7s5fe*I!d#$AM+m0+ zdj{m*&erxz6m{TVUBj=7ODE$ck*GghUp^CTm&PdPe?{Y!cx}#Wcn!g8MD>{0NEJd~ z-B<{{Q{mH)SvJ6`o07yqwH<+l>Lc6) zNxKUR&&#k4M0i|=LQ3{gUc6HwKzVG-!Aam1?}g;de{mXqxNhReIhn}oFeCbpX>I zCOpk8xkE~IVmih&#sC8WNOe=NdvrNb_BctBsUD#jKoul;36!0PEr||`M=8#u){>l; z5M+%la^Ugu@Db=Sbtb1hjFvW!!3q+cEU_iXvLyt~45jcN$#?O=f)c zWk8w$#(jNoHB!q;`JsjCE>VZlW2556&S(IYGbRF11T2t;$Q=(VVOrP8S~6L=QbVG3%xF1@bH;jJ<=Gv=NTvlr z0B8^&0MdZmxSXt+fTCEyPRx;1kNM>$!&@Wa`l`{O#Fmdp!Z_%5W3&u-jAKA)XcK(nVzi2l zwDjolh_n+VA9xy46C(VV@IvB0TI0wO$+o6pio_zH^Z^1oo|IN%{Z}1MA|dbr%@GlB zI=aM;a|8h5Jtbl?_*Jl&8CFSrMktfS-z1L#HG7ENhwU_;P8d;7aia`*RjF=QnkXf> ztHhEN@s`*TjF=G&;5C0s{4@%zN!-%m_N(pPBif)85;p5!2I%Vt9ARl!3o8^F*$$63 zj@}-YH2!zJqWV!T;{IWaLi%Qlv2dLcv*NDdR0}}*;`cgXpBGMs7Mj#XDEpwkhqE!l z#IYz%4zdC+uod!bLPy4uv`=uLI$K|7dR5&OcZf2MhwwxDRL%?V zT3(#=%CqwGd-*;gPYK*KABth!Iu4m`1x_oMsW#s;uGLeC zfv2Tf=H(AvcV4tLF};(sJWX{;sG@0LIOgrNoi}y@%xV==8J4e5c4hitU%)Ku?5lO0 zUi`kWxJ;4xh9Tq*WqYpNXXlr!rhxEkC*zZGdWP5&1OCMbhl)^w{kW%FNk6DPks(0T zr!)W(c<5#Tlw@^Uu6vS6I1P@8PDa7Vr#V?s`T>%2FdYlGNF#7BtnlJYXciWmOM7zm zR7IT=6`Ur9DQFE1M_9Z>eXThEFV6pq^Z(*A_xyi4{-hoNkNAJp%z8C@{$GdJ#rgl2 zaQ^>xoTMa)v+}DN$Lz^}j6VDw8Kb{W!qR>fTa2>?e*yr3IVaEXw0OME8WIF3OIYdqNBN+OHoNo;bo z9-ks4XQ!X&ST8=dBZGLVU+}hX1upUYIVWVv+C2S?EZw&YQAF)7KNL#3Di4OJBg$b` z8qy#Kk~j=WK&1{u!V7N@5mcQD`^#u|fLK;G1tW zvKD=iRX3j7PixnG9PFQH*^L|jl(yYZZ(5zTRHluXUvZs6YQ1xfm6BwD2B6eEt>mzxd}T-v4v;KhvyMs^-l7zm-D&`%Cpdsn8{73$y;% z-?4AGby)Sup6C9QUn1j?o%J)`;`a(ZS z`8T$Q3(7F~ zN1UTcuTRpd`=HWXrb^B4ps1KC%{#TYyv*DE+FV&t-jjk5+i^TCXIzQzS;}WmGzlY; z?TP9F_AaTk^*7pouk#EEB0>$k5RJPe&&J9PZXAEK{0 zHw9>N&BapU8yc%vX`2q#@F^XkDu|CQt&4X;38#ZH4tiM~5`e(cx^he}rX4rmHRgGs zcA&2`7TcP55#4?*H>fy$dlu?^br24-Kz-jZ{(&_@3u)Cu<VsuKz zX}MQE|AvL-Uhg@6<&FD8pcyeJt^=b|Ef63Ee&QZK3rEjF_&XBK=!;cdfKzr#CUDE|Sf^sbPqR|$&`+~c>G0oXh5t4yngS-Z=MBmQ4nL7emhk)f zGCP~Ipl4*vOk3*2i~td4i+4~7y?CEP7ueV0w|RQcfG$IqiDmZ&6s9CPahLcV$jt=X zM<;g9UT$pfbhp%LXUjMt>b_VJ!Gep+a*~vF(xex{S1cIey$P^xiQgFT$!adT%htBX zcvQeIxyA!;OsGZ)b^PFx-0w(Mj940|mT+%$V(I}lfSB3CgNR$`z|P()DyV0eb3FW7 zjp+w;-ExSjcFuX)rNuj0QQ^@sc~LwSWaoD*5VimTFk27auo12$B`{J9=?%PAI+SL0 zvC~3Li!G6`hBHG-VH3U;#NJF;2sYoNJG^m!UfUobLc6l8KF>Q#b-zhKq`50Bg%bkC zDh1+T1Xyf(O@8IfIFE2&sZ>MRdVJ%Kr=y-dnF1VONvVOso5ZlHZfCNt@(m57;`xii zEzv&Iz|jJ^u_K!7i|f)DfX^9i97=FF9q%2cVVnbr^2Yp^?e6S8 z2I_el%pH?}dNYvTzfTEPJkrRp**}E`I&6V5el5cYaQA%!sG)l8=t@I(@MocNuV=y| z64D9p*9??3DOM?!_fN99^wS>S<2oqQeTs%Ld9;yI|J;inu1lUi)tP$X)%f9hP#~{f zs#FS93yKS>5Y*UuM3W!elK=VO_zsKsDs2?409LusPZxiVM!fi4|hL+3a~ia z(mfA#;V>}E>pT!iDO5^^Kt-@5wx3c|pdBLlvn>JPH-X?QDu9rbB`k3m=MAzN3YWMQ zrdUz^w#3`7=2VWyM2A$#FL^{23-f}$$=)QdVkvVPUZ9AX5Oq9~sZ&f9O+E^;$*AvX z+LL9W7B$hKIGyF@Vslr&0*!!IKZ1H(~miRcRcxp(*(aY`MSIL6`_j7^OM&fE`IR*(D920r|J$3OAfi6wxA;E%5{apAsKWfsxTwAlDN12<~}NzAG<%P0Z}@<1r$ zBnOM)VUN6!5>+Ge$PNR_P^3|k?s^`!%=T!No%^8t&-UnXWJ&0C6ire@XXHi~eoU2* zrB!goR>buJnz8(=oid3;ls9O%a@+_8)5GR__Rzy%@hYhL+=?ahu;kXxSu#q`4ordf z{k}J^;d?z+qh{MrlU{lxbPmx!%pFa4K{2}50PjUoH4I|8fFD#?pTl*N&>CJ$JmiB( zv;$%07Z`wEn`?2@%n98Hb#k@}->?rS{EC7TaZ%ODnVQ5sPKPg@y5eaQYu`gruE(p2 z84!as3@5lXg6SB(jeP_4c5+l?7%?v|(7NZgWI!)mz62`tD-ARjn-DFkhykNZ^o?aY zJS+V|ADyp02Jy=-P>8#M7w=pHy>fX6GW{{6gaXT(-#29eG6;>fT3K0@YX%nomTVF} zQ8@KtMEr7#-^1;l0W=%m>GjZz%+?m2hY2yzL=+z80ZVrQ*IWR>uLQ&hCqUo>s2bnx z^-w=%K#Z@_RgFERSF#IS!xScvM^(G@8yIcc)W8$nrCR3MvxVof=jzm3T zn(BCPIL~>!Y$<)$#B+91Vtl(_pMec-CSzL?+^u(H34 zvu6%U8G~Btkr7D?Zw{0cx;z3Vp4>a)xfakoIs184XLh9Yl}_7+vO6n9d2p)nVuN1l zDUZNkNzODzS0ptmMf4oQl0Z7OM?@kpw7I7m{IXp_ALc>c5cAv$;!H`irzr_Lm^UUt z+EMbsm=E?mZAYc)8{`Ec)AOfDG+o)Kk~S~i!+DZQ{wB)hLHW9|!UC#7?>#`6NxA?o zg^%MaCd3s{a?oxrVp2eSMuq=?t*uy@IZnu4O#=7I^QYnPR=qYwc9GxHs&sLdEhV`a zW+itqBM2n6mZm6QesYL%4(cUFfH~l1Tc4e+xm8u_ArUt_mN6zpd=SR$vs_rH6J$tJ zE`D)tt13q+@-?HI%*D=XE_T*vQu#J9zGG?N6#PRNNu(M@wmB0Pc@erN(Of9OPH*7?yQz`2OFPlsj`SNi7DwmJB`b+mz`Qj z%bP^UokSPR6ke^VDNI)&I)mY_sLH-bO$o1Co~^l(o)cxZ)wz~kczn7ugH8db>CUHh z6Yj&6*k%Cc2~To^nMtg33;nF};_2eOoj3qIwzr|=?B2$QAT4C~HclyJ{+ptEkf@ov z!QtJU4UTVYA3f*vMbV5dzCJ!}%IV$Jc<5nzlzENN+dt6I6Bg!h+KGQ(PteAlR}Hn6O|`*LeD98S`$}RS5W3SMUs$J>8jNp zS{8adW+1DfNRy|qtz$|i*_l`k&{LCxhjH=@E=S=Z!T%m}_44Q3jrxfgF97#uMJAc( zj5}OP7u*?4NjfJmXN4?Wy^Vu|jbC-e(z{PgW7i7!ZMms|XGchPNXLVlJDydXig@Tx8lBwH zJ$n53*ks&a79;C&_i*?_v^-+oy>*h zt1Rah^Hm0XtTZ!H*a)%sHh+gcduGUNXT19U#^#S3uewPasx3~!yiZ=w)>M3y^6M%E z9%xt2_(P5NC#@x>De}{SbE8-D`MGI`4HB5K8X)#r>w^!OoO&l`Mlx6;L|Y2@QZk7q_c zcx0|ojDLy%C9MP7yE-NUo=!?Z|1r>R>~QM_x#Pz^_9A;2T_!t3M5V6lbd1j_A<)G5 zF&*4T_6Se)Z9Q90s2<7f9g8`_1`{T|IgIl@R#M_d7iH#W!)8hlr*yVnU_bl8C1?P2 zQ@zAme)V{dU)$o5m}rF;JdLSgK;1}QBWLiU5=%#y80LdXa#U(u20)a3pp3=JW~ohI z#QK!VNoHZgz==)Q)*on+#ly>x`NtNxfAbX7q4P4Jjj!Ahnk{A6ENy3gVa~<+70B9t zQ05Om#xfYs8HbdysLlZ)S_Phn1B+6|tQf15UxvkEW{OH^7{@;QagL`4F)JGweN0o2 zvA2eScwq={T?3B{b5841PTAsI@eUv?JYQGrVd*|%RB-0W4-^_Kngq0><`7T}_a3qB zV z0_!q15eWpk!JUD8s@KOC(|(xQi>Y#6(4T$%(6$>XG#fYm$Qygu3oW$|a$2+}%orSS z8eG2c!>IgR+?M9%701mp2pKh(Aw@C~O1j|8NT?h~-P)ii@SC6_(1C4uJEC}P+`>4X=jj1aw+tFT{__|F7-=p5oEI<4 zE@L|Y2Z%|Jmw0)UDT0V!yoBFm#AXGh^pJu9#;JoK4_Z6tlz=0qdP_~I4O1wXuL)^%~MlsBHhqFoqaL#9rpE&{4FI6D%2ZVAbMU<{4-Aj1M)KPwd;^h2}6yO8eTTM zaQmP(hH4aG8j4((xgA83L!AQx7wZYX2}iTICs{XmF~m|f;cHt{WV=S zsKxFy;sFB5_^Hh9#9%rYfOz&GX5`L1wwIk86b1#v@&rhEtSez+L7;^CgB|Fd2bMLw zlA$6L7~bRjknUy7?Ocy!%5*mb96i6|$!l6ZF3qb>m8emTBX=By<=6hmRSW}*-^WH8 zT*UKfjB12?se-WpN<%|CkhqbMzOOGXOLm1~=n~ISbkN@AAu>rbUdJjm+pgzI!8?rWa3c@S1Og`b|hg_UL{O3uYCM%wi5os z{k>*sp-S8`@(%Zn!$o;sa&KJm&4IP$-}+WW--r~TsDY7v*?lA63*-q=&Wz|{B0R6h zQxSI%p(8rcJbqb;(v8_HqoQ{+3naFqXfE=lkxE&T$fs4{B40^7$OZHfL|~?Ox#6`b zjsu6#C&oyR=epCj2WXhyo6HgKi@&gSa`5a8) zsW%Om*qH=cK7)mXX*r|sn(E@ndlFxX1bXm}`cxr>X+|hYDBO?9-e8w`=ka}DoZ>@ByfCN zeDm@^%lm4@{ZYN&fM2A#E?#7URI!-MWrId!{LPFcchBEV{+zFDRYV`I(Xg`k-;ccfG1PI%;=e*^?Weq17J{rk*lm{0ODm= ziHEM59;nNMH$-k4+MR3czi>r?;|pl_G4`wiEspT|qJWo)v;JG*ngoJ@NI5IOU#d>3 z3;a4eq&aL zpm!%{;<8hE#BY!a7|*{|V*6p<%gnxsotZFu>Ni2Vkb*!n3)D%;!PAF z(qeQns2ZbCbTnyJ8Mq%@h73<`OYF6Ojn@XRW;-0;5&<2f3q1UxLpR!5uyIPAQq7o{ z)dmT3Fx6te4Ec4O4-LM=j(iamz#+~wg!hsW{$5z(f)BZTS-7dyOm5T2U}k38a?$E& zkKwJON3XkV|6uRMPWO$jX5%EkJ4*90FAP;hMd4A#UW-%srI40Z?oC3w2zINUt&3bo zxL2()^=;hMI@75ea+krB{!lM<=4ScKc4_`*;6^lfAG*2yl7X-DJBB{=;aa(T4&$5l zO8~ii>5j*4bP>2-^selOH;4j8d^l5<{_M(3B(Xd>Se{w_Ujb3BJs1_;Vy9@J^G6w_ zaPLu^hn+FT^+%K>Y)p`h=nbswrts|BK`)CV-yeoqsK{D~%18{L`-XEWvYC?PTa|?u zQ?}H$${Yj8L~l6(0g2lL=Q*lDBG1u6lz+3axp(+WZ>zh%v-c~eRqY*Z9K7lttz(|T zSZGbyT$Tyq|3e|LtkDmUXO{6V=s%=nt<1_ZNq`GN;C>+_Y^I*7`FK(c7QfxaIt9Ms zwl!=udj^$)NW7TeKWJS_crzJeRXIW($js33A^j7ok3*hKm`@13g&9FTGIt)r3p^eJ zg#l>~UV|!3h6I}ri(sV3l^QoCcrD`|RFx^33`{B#&&p|n8S#L{AJL0x(u*YJ>e{m~ zHMvSYmVBYe)E^drW>7b!0`LS$RLL-MGkq%Zbka&J3qIHqloug*iu6u0JMrXPE-<^( zI0TwXMM4}V)<%&}5gsT~w)yCm!rN2i zXMCC+z>1#EoXrzVCe`y2XFoPL8cp1~3Tz&$l@~1u&pXiopO+uf9ad`Hr5W3Ul75<{ zOY4hZfj-ImB#Hu)6AVvqb;+v`bq$Pic32)v&KVvu5tzIMGDO%aX`X6pF|&0 zZYJtxd5~4`(!dPPXuOouKb;(zXq^5}1gM|ITq3g>W%8hO=DXNQRbL^~lCp>N=Q9^D zr#C1W1?fyH^Cl+k{4guMk!I+d=vdBvO;XdV@zb;H470`priZ;-Ya~4(AEUL>cLb}% z?kYUY|Esh=E|$P1W*(;3+v|%s`Sb*fCLUpO4yD)a&zM!k)n&2jzb2@Igi6<4$WEmb zdCa4dGXsxXelV$B#uGU{DWJeL9K}m``QmUZ_I?(>IR>cbK;fXE$`T8KnWnJ}|1Vu+gD;;lF{%TTo;0ARe5;gEo2GGq{tI}q zMj&R6j4JENpo5jvP26{)#It9m=l9(HW}2D4(uEiU%>EsFgDV_@YzE>Ej#w8@3^M2_ zj`@M{xG=8%9h;{F^EHSI&Y$-uPojjCKZ_!0L+vFXpzCAF z)YiX$PN|J@%I$bCS5Qz-n9h?BA?sc_gJ9`N5T}vhFc&vO*&oc3`2_C7p*)Q{$yxkZ z?vx_JvSpP!&4}Bn!BDeO>_j7PatMK`+?G;eqf&86&u4vEXTL12EhMHopA7K*B&7?? zhYhkWCKAOYlh@Wb!jQ@lHykM_hrwfe``ul@ifr^&$_YUx7v@eL`_o8{poG)7%4`>? zB88)&$oN0y65aBd+^qUs<&jC7TiXZNRPt-hSj_1TR=O8$AGt7MGP%L9kvgL=pR7GP z6k-tKO#~0{(q`?WRzIf`x|Z=#09GPoRy2@1(1~dvY=&IDNi1mlKfV$hES}z+7Px;B zfaOqO3DVWpzFu3*Jd(5W8ohHQ9f&Th2p69-bHEMIvHQTksBcQIe(WCXc6WL&wlR>V zS=SX@NW}ooJWS%F?v+@AF%U&q_P`G%VKIq}$eA@ul@d`N*y>5yb(4=^Q%D&W=A4FI zgUp3%Ywz%za|==pMvD$_@luIP*U4E~i3RH7if5^11w_*ctRcWce+(#8lWi~FB^_M* zn!m(kXU1jFZ`|_^Z@B|&I*gL=PCDT*eq&W}G-F$1>Ip|uaU~pDbgl8_07%~LT)lNz zl;86{4k{sth;)gflG3HLAfR-&gh+Quvmhc$iZn<|w{)(EfYJ?uuyi-VE^M6NV!S@@ z@9#R-^#FhDaGp6c_uOaZoLP>$;jCtB6l7xUQ~KSVt;FA=Jb85_zARrfw#<$pj(9hG ziv@d`(Y2N-nb_gNE%9s39U&HlOw3Xg#Qsu?X{n<^hadA~ufN!so2E`waAnTBEIv>D zN&TGwbX@Q4V^4);nfTN*{x6-W6zg0T5LaSa1U}D8f-0&G{K(6kd+Im8Ii4yEJLpR; zhY7h#4fV)Z~etW_O3Gb+Sr~Re8fU%!{ZoMNki+w@|cy6-S3%)NiVq=68(hj z>oRp)?=57vAU^J8_$fAt4AjM9zUG+Os6DM)GE${>H~uO|ed-u|S2TG1pzt-#=oiMZ z*1=NI)Y)ZPKev39qRvxOcB-SQco_=8HgdSx^EBoSi>P`Q*TGFKV=4FiM-uWTk2N)Z zMocYIhA=0oqS>TlHu&z|5%r_$+K)6kx%V-?PP{Dg(s~1L`{R$|Q*<8aYkpnPanJo3 zB*+EHmS_#gUvw8p$WL5-wfhawa` za_>e`%(V=9TxGjAmq54)4`4{X=a%`{SpV6^ulEP&!1`qI56(8nbZ-(oatyNOEf%CN z3qB@#MI#@EFP=0KzU^>u&0g_y(k)q8Yu=iHjp)xc-8bhjFB{EbTP`R(i?|T(CUA4k ztjpOjJSzCBryLE5lp61n>?I3=kmLuZiI*1+HjWS*>5XRFKf5cQi51?@Ad`G~Q-$s7 zhbj)%>gR**_trMOV+2qL;Xzi8gBIODady&vWsjXZbXp%j->FqgjSl=yWBzgT;-T9s z9?SNh6!m9|s~E``uy!{6qRvoN@PqgmrO@Q@55s%GX?478X9rrZ>>zZs!tDB{q^!M#0bWGGHmtJ3B_0H{5x*Gk$~WMC7f6yuVu~#9c(Lb3;EY8j;~+8yFC9uy#u!B9DIK>he>h$^(~%9 z5$(fhPG+*R#o6JsfZgd{9-rvZ9HnVWc7sM}?vm?OIlT^(;c++qN4G4~s~vY6PsNPR z3e0KxIC%JKIo@B&3s3n)`uNAQLzaPXYwx176r~9S*;yY~jQG}c0=1jb-PH$p6Tu@z z{ylg7mFHGTzDC57jvQW^a(PrQ%h^RW^Pm1lmM%j~jA z`)d`#smcd#SL4izp9!zX#2>3JdoFYs}g7-!wUQe1}H6E6mwh}ZAAk_%=XTkL3AwW7X83*<+@ z!wjft%rlf^uZ6jHxSXi;+``}cf%j}UBE4`fBxns?9vOUHF3*`HhPYYeTdw+bi7Dfi zQ{UMw9sOla&4f>jD5Rnbjw)X$7^F@@IPH^2=sGf ztCi<6Uc!FT_a{Vr)}ji(Mk-mAry>fP3>lYT({@pq3B9df^;Od}-8cx*gczj)oTRf? zTBg33=!A_mscfTPuP~ubQ5W{UJ|C+7NIr3V*Qkm7qNItS+K;nmlvG8{&2m#s{u?=; zScyJ+xLb1=;Lg3={kog9o@4KMKp}I4r%c1dvUm0@!AmbKT7wXm?)2GSC1;@J;irfK zS3|i2KR;05dstg!>rQ*&kYwe|J*SY@c&JloS1$>AFquSsWLim4bbo3_Oi>Hvu?4pr zp`|#f%dupDvjpvFW?T(b;Z9Esb*K_pe*9RDxjmfl5>%Q;kg;auE%zO!;H!o`T%q@4 zdHMHwT|*7Nr5dojf8<_588*h8IG$kl;Ld&M*hZ(wQR+n4U4>}anGobomePNra$F|V!};pT>cK$%h9aMSsiDg zQ6YNH!=8y1f-8b$ZW)6~eajp2l)B7vdmmZw4({wmrhF>S&-+5rNCI6Xb0yV2lV#BM z)sj9Cyb?LsD<~Q#z->LIhI1I?7n5kLaquncD4Hp*EcRz!kVsVj)!PX~**$nqJodgZ zwBA*+$xq@uX%KiDNgx)b&*L z!Hvrsy6V3S3_k0drHw>|LbHQtnvSBs$f&>RxEM{$^l0;r1Is;IoaQ8PeYGOT8VY>3 zFC&XN4P zo)6jAIlo|lIt*+KH|>4Q4)_HXXfH?zbR37vir@;D?dSg~5S6vb61{2;^a z$}-}xitZ5mu6_try&hJ^%p407t*`1XH@=H!l#tvMnpm(novCUeMz#GH=LY!Sz~Ff%2|DG>v;b{|DHvzfe16KP$!R{ksyA4Q$;%SWWB~Ixc=(w z=Zg}?RvD{q3H3fIYbVcXvX2}T7=+(@Qr+%FZJf%zWgK|`?XzvX8CPL8HcsMrXPui} zt7JQsc{8e&PBk86N3+{Vpp-l=Qo_xK^9DN-&R4Dtn=ExQ$r{fmMW2 zHs5bqy^Yi9BX4T7ni4*&qPJq5)9qWAydQdcOZ(?jIXWJ5Y0_YQZN;t2?JDXa*5bdxc*K_ z=tjKU$5|{8EhkM@Z{*K(G1D z^tY9i8&%9VpWc4a#hHE*3gkWn)5)v8#T8MBl0CL>LOk*u-5J#ZuLYaM=tNL{}>DeD$tS-Csx zx9vt;+8ygYus6ALur7jxe|KKe{eI(2%laB(sZj6*yQJ4{tCuuf&U&qecTKd}THN4^ zf5Q>=BKA5c8ZjYp-5lBjoz`u=bwAeelY8o~x2eib>3zSRwnn=hTn$B3kaQf#h<<+f zJ^icaL3v7m!le3_cT2>#stMm2Q_x;iNPid{=7J;R-)MpV(rv@y0&Zf3)6I6>h0h%< z5JI{I^ZMq{;(Hm10_ZJks3g6Fdq!*E$?A{nHknU@115`>@nr85BfRuMo8YfOPZd$> z(zTBt$b7hkA^BY_lUKX4))t*HvkqQm4TXa0-et34&R%WdT48H8-g;C=-##4 zJhA%RJtCrFKLcx9{a!($n+TlsvG-GM56gbBchx59v$GavqAacUj`4VY;=rwSTQa*j zF7z7vdzA}>d8p|hf!VfW)`6<*s5?yr1gXpvCFJyHT7kG~x&$N7n~;%)fjotrPvRcp z(+N4M`$0G*!IMnQMs18atjUMu!W(+j14nIy^&CtwPgkjJ$$hj`qz_t^Hpqvo)hBbg zAF}xn6X9RXB^=%o+w3IFned`@WH&j_u6ew8l2~y+bN|Wnt9OUDGb@2n6h21CC@TCc zNixd!b~Q-r_g!%=J$wL%Ql62q>Fu_-#k2zISwL(WOF?U5L!5QXaBVl0m%p5KL22X8 zS7Jkk;cLaVVbYCV$Z?;Y_xZ8S;U%{5(Jl`>Q{%2{g#Pr9vR_jUQ_s%AmvLd)efYdi z%)ixjDBUMPq9^!;j;ozfYSeM($F>^<(Rpcgd}29G%d>mQ9(k>PsLELcKG*U?lK-mk3sX;M$d?fUVV&geCz4gX_a0Jo5#=_p zBwL#gNTHwQA+}|0G8GFG^0URB^0+zYu%0@VWXyP{5I6N6Mt9;WoCxAX?ZNk5Dq98i zv@0X$=E8}?t{iKs^=jS#;&<=eX#S@Dc#R?VO_f>h6V+Yj(_u!Qrpo;fvk^$?#AuO7 z$OrCr(_U{c#kmxIxE0HW{|KD1PgUii@d%|v`Llx$gO$&VxO;M;2wbS~ta8D7HJI4W zoU*Z4PUO5$V3cu_(NIIfCu4bqZ@LpcbKTx`HX4QM?;T}K%6Wfp45dXMtMSIzI9sA}`BUESmsH@&tS!k~6=e#8jw%8@oUr|ZhjTG0R6 zrRQDegB$(JnG2yX^;^#V<%!SSya*?ogf7nR?)E;Z&e0zb?%TdzTalyrgnzrz>T{xo z;vBYl2BLe;X|EFhk=;@E?l62auZ6#l-?|w>*87$97waaYX!h3`3L{k(_!BK;J2@w7 z*nQ2p8`x*N;+TnG6K`@Gi^FRj^vooRuq?#0{0^?!VD0UQr~1>?!NM`+%Ke_v~82^w0gfg}^#i`LjYR`4_O zv%fOk?m>Ec>w2iFH?6robWx!{ol;i(Yd$yY36W1HSKex4^2vsi(WKl0=Ri&H(JyX3p={aH1Y13gXYLsg1jXLmSi=egskKC z@n9Kjvj_SQ7SAQt^FJ0~Tv*flp0NM=^4KI0FVxkJB!rcUxAYD3f!^2~aszYW)#n#S z>0_B6Eg-|a#BlHe5)Ez{8MegX>?XNgc!Fcp;GHQ?|5TPy_DSE9t)@Hv_DNYGhx}YN zc2A*(Em}Px(#i`H7{TR=)5q@aS7~0469_gMRuJmp$&G@5&BoXUH?G$&OgYTuMurYH zMKlRlRr|Q}?LO_Nn)OWIoJ{Vvy=QrItv|-CzGT?a7cnlCWUWP0S`pud*UV+2_%&EQ;5y9=lhQyPH2ex2wco=kh6i1&MLYmy&Qt!8c{ z4Da$ics!!EfKzf*q^~*mc{?ttlEq8TEsOzmp^(Rb=vw}bFQx7J3>!MWkY{`Gn{eL? zle}!%GF1NJo7RPK!%siiFVw{R7}m|uxrgHM8HT6NP|~8xG)d{58}KP0w$JH?h)$vJ z0P*>Q@-#DzTx#jmv{sZ8=#?DHV8GT2#K{12vT~vd7dvruP{TwsLYf=v{T}JPMmNFG zyYZJc>dBkVq71}8D17Xir7_r(_LeLSR%S5vp>YZit!L|bx4apZNg6Bn;AcF;r?!Zf zg5i^T8J$_!1o$j*$w}9$6@0%EZULX1J4rlStZeoflF*#f67ySJl-ur^31N)1{aSI; zC+M1Z-i;;C>q85g_lAu|*DnaX(OQ2YYSiQ<$?}~kma=pJGhS0pRbUrbfNX5#`V0=3>J#6p6yb;kEpWr!JFG-fgI|bNQ_)%N%eSNB=YI$Z0TfS)}4tqMHT*z;K_u-M-dm-$nFyq{`m8LAe zVo10xqrkab00X3_kR_|ZZyqRcP z+1X5ADMtnd&2Xx|<*d|5r4?i94jO#;jqO-?R{6)i>lJX=?)I;%O#oeo?dW0kM*G?_ zl{}2ej{ODxcgMG^cv&CH{9l2(_wxU>=VV1?j+%9JiDZU?v|3#jOo?HU2=+P|G!iJ` z24N5miz>z*$K+RTRFJlF-nLc^Ecj{Dpl`l-={=)|R}+2M)4pFnX2qw)5ISm^7Wv%L zRAD-;h!^5co(px!w4}X=nu^ z-3#N??qRRny*;y}2M3e?c?r+_g+nx7BwtAlJ7__g~7d)2NUgt9}_}i?r9atkEL(tQBB)A1C z6!S7xfED&0;3+3Us_M%3z^UtYeO-e4iSHHGYJ<|IFYOF6NZtG7}TP~X4 zW18HI8XY-!>#iw^_LZAX%c^sb{`rQ5hf?t&B`jK{h5n~2LHsROMjnYaJ}T{DmW@)H zHfnj8g{oX2L3Lr1Oj!@H>2SZbMAfNy6IBZ38yNSp{wVJdRo)dFGg6k*eHUv-$6m3p z1Y;vC*~WENq~48^RYQbli;TIouqiU>c~>Mf`%D~kCFbs?q0^&so$e8lgz4SZ<*dEI z@2;b$17!P*^-WS6jhlsSrqUA)_KXsOwVbEWO-8o$c9f$#X|;x<~jP?!8f8`jhI8JDW zhrk`wo36He(^)+;>6j@c=GLDA{}(zePE8x*k!p!RW&R`WDa;|I77G~L*q%j@&GZ9y z&a9On6W$u;D<3GFz+pk3aUu=TQTAs&gS&k6;m_qX>%`9+8E4+Hl&aj$E;)HD-xpRz zR^>0mznVgp$BR}VdAo@JwUjHiiZEFnkyl;ma0V7;McMh5u`op9eo^-@%U z2&cBnQ@WO5DifNCZ`k8rT=i>4x-L6Wdh3hI^(pwvwQMZ!A!`B;0S1e~LRcWcBU7c>p9S1^!h>i3tA zSbmZHsAMhnKjqJnr6jsl^$x4Tit~qBEO^t6d4dETyTG=_&dL?2o%d-5Nkc#t+eriz z{WA60PZ|asx@>=UPv1c2T15`9Dtc;1Lq1xHKKR|f83xk(fu<0r913o22w5=kahh>o zbSA&2Zp_JaCFC{Tin7jzXp_27nzRi*V{cJS>T%sHj<$_(nO*M4wl)W2v$@D-ZMcE0 zo=SJIp=!5-sOos#bV8k&3rGGyd70Tycl535{8(7Twz&tb!r3b6!cWG~p(MkiyaJT@ z2A=0r3|m1&QqGYVaqWv{SdqR{^>i^wYLtBL$YvU4^K5-;z~oMYZpmid2jA}UMwM#K zB4IkA(cStTItrT~j0$V2B`TCtNr2k?qIS;i;U9kS`912(tmGAe?yLB zW-JiFASMv%7#PPQ1S*JuA-plLH>Z#?*m}JvBf)<5a7FWH`30z5fk}N@+0{3fRP(G( zN_42NeYEwG)?C+{u6tl}UuCAsKK;dik%u~SunUw3VaYpKI+ZeO?>>YS(j9@o{+el3 z=(FIP>h?1_^B~dIE2!5fA#ui8u+*?X!xo)3o^+eU*R@1*X z(83s4Vc^TMgPWkD-UtCr0=T*!7!Q)wQy zx%yLnMv(LPVaE%6t7@V}=~mq;QKy1&kFEab%aQe(E`N6C^H}3?KLD$&(}4647}~F5 zs7LQ1KvD-tW9mev@xdytY_qH%Wue3zC(AvDMoeqzR7&iA?sR}`1=DLhhEr6k8&Ua|7v9M=*Owl4Vdn^qRo(ySg@kB2wveav0hRi*S~I4Usg9> z`XF(pC^bD;$DZ_FiR~nZV}VqqSRx4#)KM5;r9i@_Zp*P5+Yn9cMS&sTj8L@!0#N>G z*o|c&aR5-RflxG1S^}%xM_?HRovlnu_pQe3Ync$9(ajwxKi&x*bue@)o28vfkS~K3 z3Z`F0RekVvSGO11O0Tm`8}G@2VVgz&H(%OQV0sPcAppb`1;reoq~3xOqZge;+D2Ao zhV}GWKSj){2+DLj9IrSG0ei0c)f%qqkv+k5tC<)0l1LO~6RYNb~yk}oz3yBO|%uGuOJggWVLsg{gyOpWIob>alq zjc!j}6))G1p7lBldDu}(oq_Y8(6wc-v&sacWBLaA6>H~#DHU)QkxB*iyIn;^4r%%k>T{e_}u{gw-zD> zv4YE>L4X<#0&P*y;{z0w@(9u^Oi#7nO_h)F+=+IqH}=>TNJ})R5Ss39TM|s|Z<}=z z-nN2{Z|hUmTpK&9RV}k^{yR~u75hulg9;(3_6fug0@%4?faL+0Rwx0rkC64EXw_Z^ zZTl$s=EavUVMP-&iCb6cLZZiC*o~_;8}8|CW+C-TAC4F6lm?dUj`i8~JL&Z?BmXsG zUp(?Tg)|RgyRyR^AT2mUs{A;|>4pW4DdX~aCPl*i59yO_N=zKRipvsb%XL$QdfobU zXWdE%vUH2?kGI+Dl=_a>VLNQ^0`|WK;4jGv_E1=;c>}-*0Gf3(q-2Z!qaLgF_7!jAwm7)kVBxH=sY zZP_f;>$c z2z?Bxjs*th!Z;s?j-IJUl2kjB(4_}9PnXtCJ5KI4yqK{Q#t-n;b`(hmnXUEq@0TWM z1@~a43oh8JQ1bdL(m^-9o>k1X{w#@_Dv2V&BfKD)QFS-QfoV{zN-8kr)A;yVWps0~ zPF@|3*vIl1ZEM}TM?T-~1h+Sw%vjq;H@r?H`wo^Nhf=4Ru8f%8+qHmT67-`WnPgy4 z3GLoW}`VM6!aYRt5NKjATa z##9S3b6I%3@`owhsc|vzb$=DyKpZM)5BS!UhqIja(w#WxuUBa zjDAO({2KW?NlnC2WjG_H<7F3ztVXbJj}>8UoB$?Gf$5z!0C7T9tPy{axpL%pA9K#_%o?)5#OR8&A`rBC_2|HeIuz+!7{8jF32CdkMTdg-G#WZf8V5zKWg2eal~N>$%^{dce+Na#?DY8IaxqSOHp6MmX#7pn$t{_FImFYR@|z&YJ9Mmu|AGG0rP) z4_>3wE1zj|MO)!nrNyHEnSu%i#thU30QiZJ8dwADKrIuUJWb7FA-6?h#3?EBRYWjX zFs_cmk7A<)Y1qw+BQlKNqH^>8Z$18A3eX3VP*MXgt4|?3!1U=;bFXb;Jce2vM~Aa+ zI~`>i!;4%b9oLFzj|i9bdnkua(!7uVm?3JgfezRQmmEQBmcd#l2%!4V3tBhW$%hQ> z=dXRmV}9I8Q;%!2t{ar6s+42T6%bg-&CVM56ita``l7 z?Fa~7tUTP^Fd`5r9ic4Hp{dKF-C><@BhcLT+{Q@q?mseERX^h(sj&qAOd%E=Q=I}g z49vO<_`bkETQ_X+q@7tjsY>JM4aj&%7+c%h*8Rd(WQ9fHVr*lnktCSpxq@#eA+cNk zmQv^x5^)SZpgTZ?=)z{KIvWPM-^)(5L@H<2$9i2joaXwGj)D??AAHmh!eQI|*Y$_c+LR47D zG>FVccR}mMTfO9LOLhL|<4`$FX6pIuVS{U+6(wbv%q8P`opic}hyKHuOS#>8F!x1a z5p!luxpZS*Z^NU99XB~~?*4xJNX|o3CqBT_4WtAxy{A})BM2&VZd7aIZE0oK8hX@B zJ&L+zlY`r$lBH_?EBtv$??lMX1<_kAGUrJ2+ZVAkS07{^14%JVCKO|;x&>Uz7RNU6 zks3rcXDqEz4_^MPQYM{i4T!c$*Epg4ZZJ!)-zSP~w0r*Xa})dywkAR7`z;_Pf|b#AkjFYv6p0L+JEm&&(Ha_LW`21QRZ&61zE*^a4Ls6Dp--? zYaIloz)t`JP=nNapEAMCHT<=}W5#{xVWtTtdcb*&GoY$}9-+Wpqr z>QDA||BpW$JU9X)GXOLJwZl0yr0)O>mH-H52l#n`{!ric0M8VQP_EcUUB}YO(`F*- zmAbCdveReOi~=rrZtXWBZtnfYgzq4-UIVu&)8{6W?$jCl!;3BP+h9TVH4QG zVfkYy_6JNn_H?8xQft`nq4@@Bi3A0)K{_sRK9H60l{=y72^};IY)6Z(^JB&e_f|Q^vwW zfIYz6=+nnU{_I<$n*HWuIlw{gck<8ISyzC!ZX0{d2u7+4LKYYXLfMbN zTm}HyQkZ=epE_D^LkxTr!Z6sDzv6p36TxJ%d3F-Ew|1~xUjT=$qF(3UbCfk3G0xk% zpO4NKE;KG6dnR#+p*FR0^g0SCHrx`v|0*kY2ys^8+;%G2=DW}q7#N5;f^e``sEtFN zSeIsyGwCDbQRv=Dsw=d@)^V3N1$_o6mCIhWyciu;a(<{f8!Y>vklca!VCsR~j{^m^Z=1W@AY3E6t*RmW_I#PS7DStvc z47s@cPlWDlD*p*kklUE0KSI{9p${uW{I`&cn_p;4v{|`>~rkUrqc0@OyYQ>UGT+h4EJdyZ3B=XZAf z+Z6S;z>&#$3%f-_`eu2@`&y%Tt)<()BQ7n36*Kz(%tQ3|JS^979Pj2&w03y>I-7jG zj#G~o=!yN{B5cQ9TH<(H|Er@^bEHG+pO_s9B?G4#(;(zD^9XAVSWC|Q4ryD0*w-wK z=2f3GZk9&A=972L(7&=OlVcO~2sd zA|YFp&h1_%PRm$5<^Ew;!ULO&lNuR6*`&LY=r$mK2GfDmM_->&?@F+OP&| z*Un&bzXrf&<*<+G)%G647P~FJ>1}1cuV%`ZGN)>9CSDJCe^It(q*vNm0?l(9&2v7M zoaZ4r(E-IngPH~x z_7|&5Sz7af8qLCMNt{((AJ z`UU+34MUnr_~EV3{nEyJRz>@HllQ~NPKPV@iWRq{t!D6grLRouJ-Mu5wEEQl2|blX z)CZ6hi%B{k2le|>4K23fQ&{d{dIqD#5Ue3#tsG`qV-D}8ICMRu`Bc;y3J*pMI5j#c zuOG2d=1}Q2If`7`Hz+kYGnGFxJs(}8aOnTx^ca|b3SmwqO(i()72WpJ3ksh((J@rkG+Z#>K1WHMV zp-bor^x%{BX>mz`3ohdiY#N(9zDkC|J6m)^gWSg0FL%*qTR={V@7KZpGvz!GFb1*$ z56_rTffy(QPy&9HU`OlN_~Oqrtm{wmyU=HDPQRp<%Tcc|oO~tM)iid`W+Oo9r`oI|UND^dqVI`)~d9Yl9BGqa?#2 zmML%~{h6ajg5WgGSY)d)jTasN+sF;eoW(2jrw21r8HmW zSY-dtH(>2Ra_20DS$&S__xzB_0DaEQnqaN1KP%d92Ueg8lAuas^+yS9Q&tFM?%KY; zxYS73wVl=`Yp1g<9-f_*2!tP}@n5P6iNtOdj-ZOzKf(M#rY{De)+&Oj=J0ggnNBOe z&VghtH~y%gP0na0`s_*!+jNbGbAM_*t$yL7Xz+X)i=}@ZmU9fU0^fqHpelYr36QY6 z$EFA9vx+>=DVeoM{YwqZNqMb(cJkdye1vu6RJV3HN``-~35_~%UYc*9PPvi%4?k+q z6(*B_t^3?Ve?fgy!1$;rj;7bMVjA-JzBC9bRP>)`Sc?0A64*VkR&Weu9-2FrvPs5xMR}_!n_!lC z0x_pyJG3jb7~esqK;%xnHKYY$s1g`>y2W|q3=0JGd+tV^hF?EwOme7%mpUp-yN_t- zQ~G6UorIcl)Q>boja$U8=k73>BUfYW1_7cT;`JG_J~+5mk_7rqvEQ#bJ^FrPuMMnZ zRJJ)8!6g^an^J-4gWD@HP_fN}?rQY%B-HuHH*d3BukbIK?Op~26~KhJO%E`B!i?^8 zLq_n*MT2w(xy(%Ws4GQww7xfL&~&epXR|4>7Wl?j9@5+iLRu6ojOF+hNR}YpOpm)A z*hPas9+(Hv(Nt`xw`kIpRwU&85vaHh2K8Vqh|MkVYY$+K^}wDUfB|>{h{-f*J6kD% z0635c-FLtCWfn1@^9Aem6ff}TdRvFr`-DDFyM;Je%_%(`WmdWEke1^)S62OOr8Z4& z_G%-4uH)apk+cr+CI-F8-$6}bNo)pSw;_Jc@dO%NGs*e{U-qA@njojd7~COD-|mKc zD%^xQb*g+<7Ek8W`;?Cbax8rKzr~2GkDi$p?F4|{vpv1H+ATAjb6M4t&O_;o8XW2C@y9 zdqsaQkNOws&wtJxLYGdqkI_7nkdGjM7E?bn*@&qZX+44pon>`Brm?tSGg&iw-i^`E>!HNPC+vcx^Kmd@RYz|xri#-1av_x{6(P}oSs^A*r#>_B$n>zokGP8Sri z!>^NfV{Q^$vn5Cvq2lH)`&N}BJF{qO0l{-XrtifNJ{%MW1qY zm^*>GMj)XNu!IC2ih$k6eo@Z-Szwu9Xxk}T99uhK**7n`D*2MF8eV1krU^~T&XOB# zSiA#tb#^_a|5(!RC(H<=;2m)cZN%I@Up1*O)stXXMGd21XUTTNcM`VhI9?P=xG9sW zjP`z}!&&Aewawx_T*e0qUaH(=g|ZygL3|`2U)7YSR7Y{Uj3dE z>cv)lfI%6}I6hI#OjqB!r={{d@*SV0@D9HjZE^hn>ZcwZbB5)eYgj7{xP?(bJAmB4 z9&k_RWFVo3|8|ZTpA+sjo_uy)JXym-Z~N-3TNT*$9}e3k=$aNRH~dY{{!HfBs1&ID znM_4yE5_?QmfcMh+AAy7tW-^Su>Kg|5w}XppCNCsE3Rr`;vo|!)u2H)hZ6qk88&qN z8(yfG4XQ1OJ6hokmb3tA4oKgE_>GDI*>X?&r!Gb$7e6bGzO+w+Jx_PMWk)K05^v?@ zGWFTnIL(Rd()d3i=RdvBdsN3#Zs%aFbRhKr22_uw#2dU!g|!vANCrCIli(WL5qP?WgagbLyaILVWd#zsz1^u%xn=2FA=u1=A^8>md%Ib8KYk@s z4REBsw8R^}?g`no6#S9#I9r^?nrWr*7Q+oLNpCeo2oj_N#L?Va%$2Ry0LW3#R(jyk z{EXKe*iKqvxP@nbgg_B~)`{`GTh1Mjc|*%PuE;5qgsrLKY86~SrhB0wJhuiN%n0n5 zIsa~I$k2NU?N}Zf6AfH_oKY1kXc+eW2e8ErF)j2-Vu5ndm1(iyYMo3-_GUW1nagr{V6k`%^Ct($4^B+eAH^}G9Z#94;p#k9At+OcXV97UI~A!Cbs83DH~l{+IM2 zN-zXVT5AxCK!CwA^Z%e0zd=M|1*Ds3NIa6WKH|trYqx938;po1nrLe~Row&jAK5hk#+s{sxormF1e(^6FbO zio*d5l0iQ4h#acTn_p>k^(3}=8}_>}QzR}RqiRF^Yj1f$n{tN zHw2s{bAzCr3%$pJfx|UR0^1Ca;|wWn8$5b9ZAy}G-Q~)veeoVB+SO7CY<}X`^wL^%rZ#QtNvU=son@LRN#`WqO zS(9>NKYpQcPhiIw6+=jrYp;w=$CulGQn2D07*a)Sz9#byAi$o^C=9YSH}m5|$~fDpB}b_9hTK)*j? ztleQ;-VCbN$sih?FupVwq_)GWctX|@!Hb-IM1u@)FBTYRxxds%&A?L>6xhJx2moD9kIKw!lU0 zx5kgKu|6jMnO`l+%0DX^HE0 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%0AcDMh%UGT>#`||~+1FOtBlZ%SrXoNM@14%S& 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 t?EmIiReVn str: :param text destdir: Directory to place archives in. """ output_path = os.path.join(destdir, f'{name}.tar.gz') - with tmpdir() as tempdir: + with tempfile.TemporaryDirectory() as tmpdir: # Clone the repository to the temporary directory - cmd_output_b('git', 'clone', repo, tempdir) - cmd_output_b('git', 'checkout', ref, cwd=tempdir) + subprocess.check_call(('git', 'clone', repo, tmpdir)) + subprocess.check_call(('git', '-C', tmpdir, 'checkout', ref)) # We don't want the '.git' directory # It adds a bunch of size to the archive and we don't use it at # runtime - rmtree(os.path.join(tempdir, '.git')) + shutil.rmtree(os.path.join(tmpdir, '.git')) with tarfile.open(output_path, 'w|gz') as tf: - tf.add(tempdir, name) + tf.add(tmpdir, name) return output_path @@ -56,7 +55,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: parser.add_argument('--dest', default='pre_commit/resources') args = parser.parse_args(argv) for archive_name, repo, ref in REPOS: - output.write_line(f'Making {archive_name}.tar.gz for {repo}@{ref}') + print(f'Making {archive_name}.tar.gz for {repo}@{ref}') make_archive(archive_name, repo, ref, args.dest) return 0 diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py deleted file mode 100644 index 6ae2f8e74..000000000 --- a/tests/make_archives_test.py +++ /dev/null @@ -1,46 +0,0 @@ -import tarfile - -from pre_commit import git -from pre_commit import make_archives -from pre_commit.util import cmd_output -from testing.util import git_commit - - -def test_make_archive(in_git_dir, tmpdir): - output_dir = tmpdir.join('output').ensure_dir() - # Add a files to the git directory - in_git_dir.join('foo').ensure() - cmd_output('git', 'add', '.') - git_commit() - # We'll use this rev - head_rev = git.head_rev('.') - # And check that this file doesn't exist - in_git_dir.join('bar').ensure() - cmd_output('git', 'add', '.') - git_commit() - - # Do the thing - archive_path = make_archives.make_archive( - 'foo', in_git_dir.strpath, head_rev, output_dir.strpath, - ) - - expected = output_dir.join('foo.tar.gz') - assert archive_path == expected.strpath - assert expected.exists() - - extract_dir = tmpdir.join('extract').ensure_dir() - with tarfile.open(archive_path) as tf: - tf.extractall(extract_dir.strpath) - - # Verify the contents of the tar - assert extract_dir.join('foo').isdir() - assert extract_dir.join('foo/foo').exists() - assert not extract_dir.join('foo/.git').exists() - assert not extract_dir.join('foo/bar').exists() - - -def test_main(tmpdir): - make_archives.main(('--dest', tmpdir.strpath)) - - for archive, _, _ in make_archives.REPOS: - assert tmpdir.join(f'{archive}.tar.gz').exists() From fb590d41ff11eb65e77b736e0def12715b2b3356 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 4 Apr 2021 10:00:49 -0700 Subject: [PATCH 499/967] give xargs batch file execution additional headroom --- pre_commit/xargs.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 60a057c19..6b0fa2086 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -145,7 +145,9 @@ def xargs( # this is implementation details but the command gets translated into # full/path/to/cmd.exe /c *cmd cmd_exe = parse_shebang.find_executable('cmd.exe') - _max_length = 8192 - len(cmd_exe) - len(' /c ') + # 1024 is additionally subtracted to give headroom for further + # expansion inside the batch file + _max_length = 8192 - len(cmd_exe) - len(' /c ') - 1024 partitions = partition(cmd, varargs, target_concurrency, _max_length) From 5827a93c2fc94194ad375c25d0885972975867f8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Apr 2021 17:08:42 +0000 Subject: [PATCH 500/967] [pre-commit.ci] pre-commit autoupdate --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b63d5a9ad..3193b8cca 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - id: double-quote-string-fixer -- repo: https://gitlab.com/pycqa/flake8 +- repo: https://github.com/PyCQA/flake8 rev: 3.9.0 hooks: - id: flake8 From d5eda977ce2e4ae586b9ff4146fecbaed7b574ea Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 6 Apr 2021 07:55:32 -0700 Subject: [PATCH 501/967] fix archive permissions for ruby tar.gz roots --- pre_commit/resources/rbenv.tar.gz | Bin 34224 -> 34250 bytes pre_commit/resources/ruby-build.tar.gz | Bin 74163 -> 74218 bytes pre_commit/resources/ruby-download.tar.gz | Bin 5343 -> 5533 bytes testing/make-archives | 11 +++++++---- tests/languages/ruby_test.py | 13 +++++++++++++ 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/pre_commit/resources/rbenv.tar.gz b/pre_commit/resources/rbenv.tar.gz index 97ac469a77bbd421f59a4d234ffce3e4c47194bd..95b5a364dff06d5fc8f1176a9ee477c817942b46 100644 GIT binary patch delta 33626 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 delta 33597 zcmX`SV{oQT*S4KxV%xTD+sVYXIk9oXHYT=h+Y?S~TN6y2NpgPI{nT6EpZ!O#uCA`` zRn>df+SXJoL}M%jEJ-XZZ1=I9aWX6y4jwwYQjoiMq$n|Iz$>hIhT}$kwBbp8vIaYF zlm71}+`WhWV+rSt@xgj^@8o1<(k`hhvx#FN%Cwe$3~=MDna;NOksL5!cqAbl|qNab;0_2qD=)DYzQ4{!_k zq&Nq?PfhuXfe`*TE948OAhHCcE%w^ZgIHhuezprDkpb~&b8G$IBL9US2VOsj9|w+B z>SacLcYt_SSFIL7vnOqjU&gvd`VpT4A4&f}DDt2HFUpS5n_!GrK>yHRFd7<*Egvy* zR^^L5S=Il57X-h#@RLH)?+~fyRVvE)P)4(s4fNHA+vRfJR|A#Y*;Uk`O4!|WZS1C7 z1b=QU{`%wEaH`xNMAB~1J=2?69ScgqT&)KLj!5jWz@OG z3|+4B!qbf9&h!%*usCyMjUMPRP`&jjg%O^4ai-n;L0Cm?%Bl~+9dwTP%{a!cjbY+m zlyj6c0o=cDE~jJ_PWlNphfr1o{)0bcFg`T|V84fYR{IvYlaBatyz#YwGFy_@5=!!% z^G_tN>@LGZ>PXe&&&u3J>>suG&6*A&z@E4utul*0?Uhm-Fw6ywy&3;c3WPusd}%)@ zMG!EY*WL#5`w;3mS5>~Wb3rH`Uj=s#6c}JMD6ZiCHe)|X=9d4SsPUP8gC3G?m+%>* z>ilY%v|%)=#3{xJE18^Ea)YnJ5c2b-IeYmP8*yXVhXhvBT~-aDb29STGcD~nn_Fe% zq>$Es0SN?N`v$fa&RwcJ2h~;n#8EI)mUQZC!i`j`yZE)-7cx9~!RS6wBS^c6f9iNi zievWjdNLi_((;*lx^XY^ocKzaXhD-Uh-qPT=wq}EihUU11;zee^ZW7(5P?0(*aUUt zf#TbwwLl$B|Ak48)k#oj0(g1zd^zu=@6OaB05rnpQ-ppvYEzhmS>K6p`!;nFU&l9P z&r5~28!rDy2-AwM#Q+AsAd;^(4TM}@V~9bs*npLS!w;4@(BTJYqyUt$OMC5K#eKsM z%2;du2N2nayL^d^(xk8%ghm>ke)aFHRs;rAiB|*!R26>ve`js%|K;`@v~$!4e4gGv zoZj3o>|X7(Y*zjYIs=Vwo=RJRoN962L6L7W(K~~Q5)DGpp{V`F@_SF_uZ!vA=Lgd_ zr5FNp+qt~?VTUU!p8pnIkp0n6O7A{&BG_Y^Y*?c9BhOyKuVPIY175eVv|ZQXq+c;t zvv8olWZzw&h!J>P`Q?_-(uunO;>r79I5cxR`B8AefLO5cAGm%*PS?fgVA2LC_4$y?76U@hbpf_FtIuWc*=ylmlXP_N6xk0g2km^37x2QWtvB4B1sC`Pr-A=O7%wadS`bd$dPcUjgo^Y3n9^i zB{63y1{X+s8)u}F0db3HF!)UdYf2f%%xq3{8ey*qM-2^Zo;+BL*>%syEeOhe` z$IQ8KBt1>!g}}Dqs7IJmsdpK+h;Ecz}DBxI`I}0SOxG}1?&@& z5*i%k;qoI0FnjLFn|k3PJQ<)w5=Ero@YV2}awc3tsEV%*jO!-aGR+vIw|(3XD#-OA z_k9J?TCod~JFSb;$~Z%V6Bs3>iPJp)0s)6 zSQLvyA&Ue71EL51;mP0*TQkWLX(+pGhMbl7@u8@!C~hc*!%#=>^8}@6Uu}&EXxWrM z^vo&Dnqhv|OKDH3`u1rQs>_0xeb=W4!#|t&bG`sgu`gCZ zB=c3!y3-Yn8yOERG|b2|b+@ax-#p~>8x@5btn&o}VRoQFC{cErH2iuCG^cAa1aF_5 z?8A0CAcV4pMxNPG0W*-q`u!F>e=>m5kVuYmj<2yJbu0hwm;>vWst9{I9mbCpgsCDF zLI!O*6Q3#uVGa=w58f`6{M-%+q2O`M+F2c)cYwN1o~fQOH^G47h?R_dP*f#-@s{B{ z>LYr$@{JzqHDoF1yg=3lnlS+8CnmE$?X?0fU~^neF;OnoiV$4uYiiA|hFi6OR{oDr z0q0ry7_u>_%hqhxo0NB#n1gn7d!AawKRu#u9s|ADW!9@weAefO6}l*rEqRhlQ1mqz zgV|{eawGB&j&U9)rE_V@1K0>;qy}r${u(jTZg8|b;@rF;EFxvZOy8e7bFBG+sP06h zz!WRIL*1E~s#HY%MenjC>;x6Kz5BXk=Zz_$cj;R%y3IXrOvNw8Fm^=|IHUbIqKCuf zZ{AsmU|G@XqF+@_LRjDPb$(XvJ-JL52V5>ys60|-&4UH8f>}L*{tXnd>aQ$Ie^jPc zHIn!`6=_-TPnl{6MFf)jTdeMu%bUG;0%j7wt>?wPk3DzT<`h{Fr8&1BdKJdU`0!uj zf}y%AVX3?@<2UujTv&=!H_b$;=N1sXYK%H6{o6`JmqQvGu+pET1O6Nue0DnLLcag( z{1#?jy`#JQqwb4@l$`Du2>)nE0(~x=0Ff{~|70ru4J3X!S^79qwSd;Rxib*nfISI+ z&(M3WJs2GRSToD6rfgHLpfdIsH^HsotmMI!M+uyb_ZZSid_vHmP#7(*dWQzKU{qne zAmVPHp$Xc=+6)LRllFW}+Z&5gnzMKdvKes)?DhM|ONp2&C3aA_vwTIhv~4o}g?uk# z6Rz3-doxs;T!c|%IEMe(kns5$Ac69uaXX|7C6(0yeG>kugQ@e^N6N`q>xr z-?)%}pO^DvpEvYVLc(ug^k8T@lmpDyld(|@>e7VdQpn5brr^3is6gO1GBV0sV13jX zjI|8x10UE9T7#$Z9I~e|$xUvQcEfigqJkTV2fw+6`-P!}2Mila(8)}&)g&&%h1|qg<_w&`PfLP2=;10CY5_&`d`*tA{d!Dvu(dH4uVI93{4<7%x_- zgXs>q399JuELE>Xz^Fdmoas4q-qC*bs!xZ`H{J;q%ExX-?(ayr%mLmB4$*+=l8x5o zdlg)ty9A15ci2X1;X^w*(BjNJ7A8H2oPoJ&QX-T7`D?*JY-0Ef|EP7MIGX; zjwYFvFI%`M3qD{_ae;rqA=aX<|CTEDg~OHov}Us{c4;W;qq$}X$;;|!nJGjvD-*UQ za}vq&ma=;3t}l2*gxmJ2JwL9nbR+t_FTHs`UTNhrpU#;*NQyd7RsAngp36a_g>@-P z0azVa*NK>FlwP+z>oeSe*fKjsQ<0F$} z5BgF!jMA8}DRKx4PiQNp1$Ux{Z%D{PAP`SQ<>oiIWnyC5`odigSg(w#m6&cAGl~HfbdrYDs zKCttiAsfW4xB@8cK==SO+g2!D%OpMWunf%@dsmkP7b%S%+L25!S6R04P5kAYm6`g; zX}1;XAMHbNtPDbC2DupZ)pw6`Z8G6DC3*66(%`izZYPCh=b;p`M}Y%UA2Brmgy@;S z?Zw1U+c{#$VA3tv{9*3L-QQaUg_lqm)au8F-!;E+z8p8kHX@n{GBGsrv7jL@VVnssFO+2VzT>r`+_wL})+f zh7Dx;YKD90X9x5Mt8Yx_U5+#WLoD_PD||3S&0xqNYb>47duzMWJ#~h0ZDOnRkS)~g zv>v~G?E_uWl7sst=JG$02=dy}gy%e({^o+Gnhd|Ue_ci0_Pvgk##=;@ABxs-3pe~# z)b=RN)vXhIx`-&O*X++RL2TP7-RGk3U52KV-GKyjdhs8t%f0R7$wTV^hBgb+zq{m< zp+xcmg7{!UY6@{)sOgnv(eytn)!?9HW@D)pJR1Zb?7CsPJQljxDu{)-YgTi1c1H*5 z#vB~+H#M02Wv?8yi7shUa6=MCJ?4H_7X_B@vlF|ZQS9PcKQcV^iy&@lfq!Mu7qR_@ za$tP$v#*V5Et|-oPiIO4=rN{(36K=g{tJs)nU#ZmPW5WbEVtm!2ABY!DS(AM+zOA@>e(qA#e$$Gwmd{{;J|CpjV`)!W!2Skw~qo@p$>X)gswM%m)g+`C5{evP2E!H0JV6J|n|L7fP zuVRX>(I_P2%%QwlZNlr7D{mX;MGb5hWw@h4+rU4-4e8TR3;5%qW0Jr5>&kde>hY6m zty%Z*TX1W7m`DZ}Jm?mYZeZ4l8^|*Vb^lyx<+ha{KL`hKkC?{O7Z+`8icE=x21&XQ z;$FcfRP_mYa`!#xBrG8^IN?X^(Ie-V17NP-I6pg23CA0 z1Jejwjog5~Gxh;VTgQF_Puw}e4}smsMkAYl=*+f0NG@cCa@~>ZoKc=AO@G!T>D9u> zi!c}|Q7eL}IArCA$`!aqcIJUQ&dkdFYoZK?BX8Y}(281=8zzsMqN9Dg6{ zGT~1hrO>Jdc|KKlKf$W#DXrrv(`MC#5~QNRP{K=M9TyG9E=qWiIB<))hc*;3AsfAp zd`(^q-K#QW@Qa@qxh^l?QKR$>O(5@&7c*#2n~~_@eGL`DI(f2dxl?&`usfQH_`oTP zqlW;fu$9vMN;-=S+fK#I#hNWXwb(4NMqt{orFe=loPX6dLK_UoFT-{-e`HVP63~eE zh@yT*HrwPN91{ze7tncU$vh}#L^q2|;zCAzH0I~53b^|}b$A+>L)aGCO%}hBY%Nq7 zkqg$8Sb6HIE04MP)y$3P`^k(B8<9vl>Ei*&nhhwTi})1Lc}{^%B1Dp|TPwIs9NkDFw!uz+)z(x-5_BkD^%_#U zl~K3Wr=PwgK3{BVAmZ}YVBtWCuM#o@#Z@ypRS;w zA5GeD@kXR+Mw`=1@jcL6SE4`!XPz_U2uTY!nmKue8i&b^Y^->y43bFp7xXU!AzPEn zbyK?h?k)+CP%|!QrTxWrnpnQS`Ng=cwnCC7!A%sh8qwpC<1Z^DXY!i77d(Jz3_FJ* z7V|UkpaFlI+1~x;!epr1#|-&@zn9xb4=V(_Ti4GbB+Qb=5eJiGr3a z!D-=1AoLxrIVr#VL9m%%5op1i4y+tA@|v`Gt=u9i7EST>k$!}MWniT=w8`I&`jxEl z%DJajL?>y_F_3r_gHx8&M|=$@jANp}R_AqO;Zaf%iWs>j?%`ML&6HRyMvvymGZ^4e zk)*W**9ko+rb5zn{uKL=c;=v2pDf$^=?&6Ks|Gj3?Kg|Q^0R8N3paU9CL|FNS+uw{pxbQDdHVW(&&u14fc zUoq4$nr}&=kRN_WYWrUI%tWf&uR+H9dma(3ijo+RwO=Hl02iZ?Kqg{d!=#)g!{z@8 zA?X@JyDo2RF)HsU1%#7woe%o%DTrpea>pT#ds5qEW#DI~T;!0q-mWH>xu^;pj_pN- zGES;#`BcIz<5D&sj3}!v|1Kvu>ux}MPdimf+zQKw^GnOO6x2WK@2QA`{oo(&N~%{y-3K^8Oaky&a*L!KHF4s3VumoCDkjV)F1O4ja3 z-rB~U&^l1U_N=U7&gouHo;wo8iMaiPk9=bSHOx2zpJ}Usd3G~DF1uh9;xf}UVue_2 z(Kn(mfh(`aS~v|dheISr^bOB3|8mJiT30bI7stu3Y@BDi>IytwdqkA6;ku$0=1r9` zZetRt8Gx)=eJ1;Kd35+*rJ-fs17+O3w(OAgR#~iEXy51-WsMwYMr45UAu*X7#Gr135m+F<;$$_ao zaq}Hy%|du7Yn86kBow~Y-3{3@$AlZA2{%2TdCO$PM%#^il^^w= z9ac*)!*zZ%JQg;+Z%rww3jgg=8Z6SQ++et9TNOUtMCxmXJMZfj)P=AkiIaE1x8aK? znE!FtTwKpDq%gDgQyDCbq4_Xwu-(-jID}HkbR8{`e_H-C`aWOeCl!i(2Kyarg?mv6 zy`>p{gBYKB_OEz?R#K8)f6^1RaEUgFRV@#CE;(~8X zdc$KfVw99yvzLKp-Vvydj}TIkl78ZUFL|X2gnCy@Ix$>gx=Hz=6no%0W!Y}M0qQo) zZ%WeeMp6)*8f{Q=08Y;cnLweb6;aB;7sxO**RH# z69*+cgl5v^HC|T*If$^$l1d8Zi#OPAbaxy3fiQ*4 zQHg{C3FML>?6}7gD#kDkfCk^@34_SnB`J5x&7YKJ8cfqs30_A2*V(jFcJFyr$#V(qk9FhIwcpxgK4$aJ_VSSN7Mm1?A1Wxm;FQJF5l zZJC>?bE-s6MX?>nK_P9~v>UZbioh@ttjuXLj<;lb(e3z`DP=$d~dIEy)&qGp|*Q@v8|Y!G>nI+(N!~w5_vC7>YvujW)di z;@(j~!F%0Z)ql)_E!#b`tc-3ZrsayQz$~3E5)ngyUe&&NK<0cbWkj1UojJer+<`oE zZx{?{#QvuyGf45%%Feyf_1tqbSHDnWxVO~jkxXyC)Ja+rc${a3tK-+h(_l^HPZ1?; zFNnEddL8XA5FBHq9Q~)3o$OnurQp50@v}ozg4~Me9sF%ugI%_TmX2cnk2`fHCL=ea zQ~o&Dh65LRFawhf>$R0v%;rFyGi9}DL%NeoF;0mi#~>+&aA`$drdpdJRwBADVY%da zw4hF%BPN$0aLZUtXaA}9URHv&dzDj%U*NbQ5k*8jd~KFs|j!w!KxQO1z$N>6We)0q*^-HsLO0Kd+!_-eH`lZs;ED4uc!6IDIWpj`ov z!v|Xb+#+n(PFr%Sky{RwS+Rhy^a@W`!i4=a!UT@;Rz}6)!YfRlTo&Kix)Y@mjU0yl$b`IzddS+=ikNKDnvuk zeIs{#)S&X^(~l?);jsII-5`#q^-6e~9ETaH{45NQ6-_9%rq&b7Wa2)rB0OM}iJ4HI zQOotP)Q)H)#qN>MKSyRr{@yss(B4gc;0E3X1Za=pK<`d%3vX7gk64C&`8n)C3xbXL z9c)d|M)5AwG9%QG=Thk_64knGxSX5aRI6_`D^_RU1=GR98s9%IH4{7bhmUj3)a*?7 zGxg-gz14L?N%3Ku^=c;Dbj*5GF~A{tf09ZFC#VqYEiIzIJu-@&{mV^V|FuC)99J?5 z2+HZEA2MlOfg6U>0wF{240OzuWrsY{+}sgs2@4?}ngrP`t;{p$QRgj5qU8#XCr{0K zWsA>Y9KtJc-znDDEARVU(5Z&(VHuLSUJ|%o#D5DFx%@;c68de2O=OR~(7gBCIM4?n zhCBwsRzscnM&Kk1P7{`u<}rq!s>v1sVB;LbUh+5LhQ%2V*UKT(lL=`F@(jwBq=2ubn)WcR82pWUg3NpyhdqSE_LB#D&x*#LH@?l5_$u zxK$KvurTfDuq{Ck#8(~ zl#s*NWb5A0Emo>OUt05&V*?`(e%r|k;bS3Xv{JNQSS6D z+2-D0lJTHl&t<7A2U%|Mb8)4RyFYCevi&kn)7YaH%;lx!F2`m{3+Y&FIUnV0-hS)& zdYa|l7lUt;&^HtQS(S1BJwgic)*JaZ3U2HAh1;H6ZFMP%y*Bx>+e+l2Bj+GeEkLgG zs>&$YNgww5uSKpdzw$0L@XPRqd@Xd@1@WK^ArTjsiBN!7uBPWFN`e-%z)V`;;QlGI z^wYQ#BlLua(b(_E>MzQ@eGaKV!h;~?^z=YZq7I6KOuO%!2TMePw6aN_IVA}{j*nko z5%p!ShxR9kp)3;Fo8kI}lmuv{Vi*sN5U5VwWnU;foYZD-{)qT}0z!$`-sp)mY`!e5 zGyyp_lq%ac3KCUXUE8V0uJ_BL0WSU@iPSk30jOt64umDdoNUS-cii1U6+Pzzx=OkH zZS0wjf#`|SCGREKTuZm17{jX{H6$;v>g{_ZTl18JIJ(})cZu{-eJ7DKxdqwGu1Ce& zh_s^N_3Cl%I-Ip&K#i5Zy!y0_RE@_ih2p2ps#jWk!% zs0F5V3U}>&&TtQ>q08@rE|)+>M22pkKBRE$S+w{8NPJ`qV6A}EGk^6i_=vV5JU>kW zS#?T#A$c(|RVTG4TpY9aY^a-6%zKV5H5?<7GfW`V+<6xPu!to}PDygyF$1%ZpDo(B zfelBpD{_qf2YIxf1vY}KR*I;FG^hJh?*(~RYZSzA2*SDteVyNflD6Jm9rpv*4U7x& zKwf+VASXvhkI(OBeNiCoQGn?j5c`iq`8E%{KcAd~PES8CKvRD&-^U9^>4O^T784VtU*A$)ly+ z{2|iI@>fJ~v_fvFuaS0FK>V6`z~7@8ba{972dWjq#J;wM^!i7}>E6-wY2pNpU&Yy@ z2hM=;cACoB!P96L?JUi2g<^UD4awy`^O}7F$l?xWKE-jKd@5t977-tV(lWATdM4Ls z7&dq4Zl!HxIcE`5Nl}*CLb&)50MloB1=a3Or&{%J61(^)QPXhA^{w4=xP67`6OsQZ zADl9Nhn>0R^fAdd-|dKZb5&ELAhvwbBjw)G{RZ#= z_?VaQf=CWSXZEG$iJy2t$&gL|aY7HkCG5wZ4$~Y+WI0ug1{>%EVHLoXfQ-kd4w2sn`q|q+ zJX&k;Z?~cTr~M0WahKkj#iFU0OB(zM-Pv67X8LJqSCW(6S!KN^zi7+}ge;4L*J3Z) zWx=Y22OFo3MZG<`+YP_8a(@p1?QZYIKE5CC7GA)QA60=ZT%fPJeH+mE>#2Ae8n@>k zB5(dPbYEyjitxI(60!|h zBt=NLU;~`S=^rr$=9#ochPrf)g&#!!OBc6GBmj9x^~FT=y#e|{M!*_IZ%{v(9p-DoPnHP@2_Fd_0XR5H2N(Um zvawoYnulyeh(VJ#bTb-c8Z{+1oY3OGW}TP(&;0$rY`t#x0m=$p@9L)7jI*vxp4-vq_DYs$1U;NA{HIb} ztZ#rzZ&gi$?{Fl<>^6m8Gn7N@dbppOA2%$Mdc=j8g0xAnUe>J3y(UE zz=#|#FWRt)gY?Gp6uN#*>Q_Xw6$y!O*iu(oN9Rhfh^r^p?$K~1GassF66ld#07+}N z9xSzH%HtDES(Xp53R?lDVyujuOvOtkwpFoC?XwOhK7NO1wAWqFXfiw-KOt5ZhosU` z$_ZRby^AGQ4;R&4wShm(R74RM-QPVJ(wgD~y5F_lVbX_G)JJKP-8Aip?ICPGofT;+H$(3}@69WG;Rcjkv<`<=v%bL~pj5JoPyN)GFPca1Q_JJnQvCNO?&sgA zo%*#k%1Nfp9i}F~SgX}th0^D_CwB*j zCDLU{%OIA47)Q+rK-{{}^})7VCbk?e!zKBz)KMR#pzriPM4s$)x4V{2sH_|On~Nt%UEtn?lL$FB|7?J;CrwoU)nE5?MzQcWzss|Mp*wVHv>jp?}OW{59a%jzL9ntrLV6V_m5 z+3rQKt_A$-AT%=&sW*bINoRPM9;yz=yI@JrT2%dDpc{07WL$lCmbxfFICBH~!EW9q z`uZztzN7S{A!hL#q4P!9%yi=SIduDnP--ne(8Fiz-T_b`?K@!XOe7sN(oovii76h&M6p*+xyL9}=N*znH)T%pngK306jPiK zV0>bP)Z$)jGeWu@F|@MizsfgZSgT=9WLr%HEvHX$mK@yjjBsDsYElYlvJB*wa_7FM z3h+XvS-_cUws6TRA69cDs8D2jd}c&@T_97H-5>ME#3~|6Tc3z-WLpa}etR1#t%Bp3 z!Q)rai^0zSg{_Z#+u8`zsBh^C7i!+)6g1A{s^r=mPgc=mZ!}#{a&KxVi(eyRt9nzcffyZdii>? zqby?u-$0Ft>D_^#K*o3f83~af&>7)Rl=1(VXda&C50G?;>K(`!3IxFCQa00P4$a9s z`wC1qz~eC}LT63x*oVrpr(cHE_~-`i;0|_vwTZ@9R`_~g7yRf5HulRLg)#1O0H|UTyirVZvd`O%3 zy)P3`zoMnuc%soDJezxR=;kUA*2kqc`>QtbhQ9%f*pkx1`0hTFMDuDvV0#&eQBgpfAB^ zOtv?vyVt*Q>}Ys9pc8ANkc*nFzslu%6JWPn~nhxBg(@Q4#5+zQe*ZAL7qw{i+%t5ZUZtH-C zD1|^ia=L|WSHivm83eDzAmN$K4I*VNr;e33*SheIq3#cdedo@+=HZ}SN^Ig+I7Tdk z3ztsT0Ry!76RD@oNW3N&JrK=pEhL!hr1OGpvjw2fx$O3t1Nyo9O2sxHh;bRBlMO12 z^TWC7TwHSA13_1|_*PD1ENq^Na08T@eLVNkDzJDS+f4>w$Bg)y0fVdJpS@3~v;G&w zM#1lQZwgf*YyDD7%t}~-lTZjRBdYkcLmy`Z033L^=>mf{hsY=WK-Oo8&^H|H9S9WS z_U!DtQcaytXZ0~u+z+dNN>vu)wFnvRwXijU!L15QQRf$Cfk={18c_b3%m85SX^!V* zZimBturL2dQcy!hOo||6Ku^nX*1fgvSh#B1>UrPsQASX1MWXws1hpbREWVq%=^gjw zB+8TYakN^I+A&pc@PcB|ApA^I=J6jB01P^f7}ukmM*i;U_sW3OE6p&KQ}RKn)eWj3 zUX_?mKH|778P~e|GAj4r8ZhW=VvLPPt%j$it~zCNp!-dcEDm24%4y`lJw8{I;c`dA z0LwSatSRngI37!X?1$77P@U}Vr$PUZ8>1pzJ|EiD+t5_xChXte>84o^!|6`$t420A zSqc9jxeqQ5K3hZyFJ2TxH#WR_YrB|Cz4S_8qcdW_RjLgF_g6cAEKspt{>Z4eiu!%K zkwmC3bH(RTmtDQI!h%wjdOZiRMOk~$>IYfr47MUXYeKq?rVI;t4urdl>Yg~<_Wj^5 z%J{GZt}|`c>V(|Ov^45vw7ht$DE@Oazd!0=;3Q*ZdWtnus+5!w;SqR3$02G5 zkjRJLgpMm$0VX#ze*pYtt)i~;JwCzq;5j>RL#t@2ElP#URLw`Wo;AIVjY64KZe^Qw zqU2-;qnmax6{{LL0a%E~tG=0)&7F`g<;rwfS;U5&@k0!!w2_2O|AC1A4`uSkJ(L%_%u@5Lp30x3-`Sg0F>=3` zNK=#*gBN~C1U+20S<-j+F}JZMBn7|_zpQ|lyP-(p{cAML$0OEQ^Z#!A^eT)HagXK?id1}FV>tz_N zeLyU-o>Dby>~XpyLB;6OlFjvaF^NYE2#Q` zYeOJs($=rNT{dC#{6dXi_?BvZbn0IGd9NB+_G(DQ3vXC#c{2 zsJ}^52^(K<5YhFMcak*2jq+i;U#@Ht4R4g=-lX_mT0MRSb1?Q+K9;JyBi4&FAD)Kt zCF1Vxf*Eq`bD$H1^8z~ja@qq$?+O=wIR!ZS$b&lcMnV5RZ$KSy0KLNLy*I3bQ2tHy z$jKk!B7+52xD~7X=PSE(ptfc4tN6tModx%NG{XX$?M;>yio$@wfc?3&z;K~DTt!#l zHhp7kUuFg`mcA5qWS7<315(?4PTYNxuvk7#vj~qDEDS$*YXD1SJVq*XqQ^bhhoI+z zhs$Mt$w~wop5B0l@B?pXtSGH2q0s4EvBSX80;;4f&!o$ti`o+%7+7)_limvu{LioS zr{fN(cv|2fjr&j2~Y$g5{R1-&@ny3o{&|Q-K@Lx ziR>ZkBn7VK-Ub z1}dlaivMTa|MQuEvHyP_odo$I|K;Z42DEiJ;BYz$1h4(a^8UL5wZ5>hz>T3=X3k({ zG{EN*eBGGwE13)73`%t8i}!5>l|}E)T(VjY?9Z?oMN|; z@my}Q?w=#2g>zc@1$Ue=^4r$)dTndH=^vD1YxA+9TjUZj1XXlCd1Wnf2CXN_%rcg_ zLA94d63s-w^Ll9kdK>!gb7E4pyM9Sgm)U6NpxK6%_gpY(dB2P`xQ)J0tYX}*(fR0K zJ~w%L?Mxl6lq#Z#Ou8Qru$)9SsP=f5;1&Zs^yn=skrs^OjeV}|Z^AOytl zKT&u9fZ9iY)-NZw05|%t>2bUNEGE*&9^BPH*46>)JOV8~)kWl1tYzhQV$3e8`<{}q ze;k?>EXYQu#Kp@-lRh4<{+|BN4>g}w)unGP>J3z6)pRXWKWKM!a%xQr>SXsZNy`aj z|C)M6(qeRvkFdwsx7$VpD}%j!!aD9lkgQJ(0@^N}a_d#DrZqyL+jNB!G#gYwYr*iU znD*iLl$Kl(bT6TEtTr8%>*zz7ubP;d-E56iM>-qBjjfExgzc$#kz#~NZcxbaB}b&G zirk#E2nz9Z)s?jH^_{K~*Ek{I)SMAZ!WO!m?C|s&2`G&F4eX;YHp1#he28He6(!gH zKz{KrM_(iPxp*C)cd>P7GnMeS#Trb#+c>jf+#PhEKb1qXKX)xnDY-U$h>~NjG_s;_ zJ)@C4otO7EOQ1`^V;U$wNNyP@Uyvo|6AV#SAaT!%AmX2j)X|+}UCtAlB&}%;jTOf) zkyEk+9!pog3o zHgRwEdk{;aM`s=#VxbN{3aon20*5i8h^ckZtowhMj z3lV7jT-Z)eqo=c(GS$@^h`Ckz!1?YFoCl^n^&0O7sy9t)3Wp=TOnH^M%SZB8#b1gv z!t{JQG6HP7rc!oo-6%4BJYH?*eCU!whW-Grs|4Qp`wk6ScC17Or&RYoBb0VtD2A3R z6IorQFg8c16&3lO89Yb@))&6m~ob5FH9G3qbAGNsku>ux!vZFwOSQ zw~}U#8tT*<>XoQYIl~v&L^7-}q)60UX_lwBQeBm*bMQO**^hcHu~hXILr#62`dn6s ze3=&$Q4Ztp*C;$xxt5&)H9nuV7_a9xWSmZL+ zNpTu(aIWssh!vV$*h!HF)xubCXX!!3EMN8HjBBds0VLPEw`Wek&RxJCx?0NSfF6&Y zrxS?*i_N<@zeQ2yC|RcdS=IQR-Q9dwPK)84GcQCCB>eB%>MP(#+D5I|ta2Zi-0e6$ zTy$IF9UBa-D%X$EKy0v2$ZoT0pRGcw*Y4_@K5J=9p_-}I$cfCSc`!fn*Gs2&NPp%h z`dtpEmz>SGV?F?kTz7CekB<)arAgkPM{7diF^s~_#X*K_mW{<@VkyD>rFDxaUL8#U zW*K5}apbFLP0vhvou$H38ASbt4@i>5O7nH{I0-1&6bkFosN2_F-m21`3Cn6W@W>HX z%To3?3Ykdd6%G%{Ge$213T%8J@8;=(bT4MpffMHmXAAgi}2-azDX2`Cp%8{%v3;>V|w%# z>;t)D8spyC|LYrO5jNicpqY9SeE~hSrO#f!4H%4qK8bGsa~Pn-sm_MqEF=XS0fBEG z9^*%~42^#JTV57!Qb^MIFV$x)+O%R?R`mGAbCLv9yR|;dzv$q8xi4;l06{P9BbuG+ zcG;EjY#R;#!f}JD^TnMS+OgE$#}GM5W!$Ed<)YTrL0l;p)Z(t+iN_7KtD-e292IrC zZkrQh;!pJOg+`d3E}{$smPJ(r)s&`>i|qS0y7o;{Q+EK0jPX;Pp}25DsPeXqJM&I! z;)G#86(1hs;{Qj~SB2FPC{5z-?hpv>?ry=|T>}Avy9^K{xVyUr4-kU8I|K`E!QBt% zEcfpJ@4ocI%slotUENh(P3pCl#y2tqP&>`)sUl;0me{PfOFvsPbY|Bby>cE6Vm22? zR;^gz(KsYOq}fwYUQC_H;}|GK$CBP^OYnZRM7eu#DipMuVi_Gz9uK3PlpcK3NE}pv z5#T8|E627N!3Vg-?@0`;+~XbVuWj6`702*YsZ*--p0{gn+M83gD(rQV1@oE!!p*l2 zGYHi*)|``z-I)ldjhry7I-8iHgv@l;EnG1Syb69Kn`+SDhmEN_f_y=n)`UNJL9Mru z`Im8g?$d7RZ)fe}2KQQ8p8UO84Vi8MYjDuPiy-T^-CUn2FV-aF!Y}=f`$EFIj%#ec z9@z5n*S45K`)0nDOh3lwJtf@%?BcD3n|;p8m?_4A+?Fhx8BB)=q>D9|=?{x{))je& zO{q6o%y;79po&BYUV(TeXoOVM99(g5{{vjn*xqXJ4+s)}R-=QYTuU|lg{q(xwR>@m zpL5>%)a~=HhO6a^yQv^wgD%1v*+L+Rgk@!cefIXzhNsG+o>g7G>!)S__O4p}Y`pTm zhN|Iz_snx456|^=YM5SN=dVAMj{#l1=vxT007R{2O@4$^g^}UkN7;I^>6}m5B$ZgJ zyl25mT4~rqegz%-^u^IjgIU+LbP2B_o-j-ThC;WoXuJKrk47_d)^`tExlLm(t}%i6ZXNv`1AxM<1t}ZoZtv!7aH(&@AVpF09!1sl{CK$?RZ zdDQY-($&}$(UfhAwZl#v(a-|lW|*rtJZwae0~;OX#novJ^AkUN;(U}o;veV{N{om` zDU{%y?czs9%QC0Ohy6$?dwi1}m}#;y_)v(}f2pErc`C@5%W=b}w&tkMq;`WYr6}*D z>@EWU3`-Mw*Z&_A<_tkJ=EK-0A*}45pL>Rf*9_k=VK|8LXx1mxtY9-IY5{?K1{34) ze39nkeS_JWF`C?^Ye%}|p$?W;$}gjZbRIHf*CVd0-*~@~@pT!DNZ_u>pxnMEJYF4@ z^OyzU7BeFE$F>p*^4nWsc>T3NZ zzsh?FU)d}tav7c7Ap~vdjX_*_zSu&sj@0?zhT7w9g#k2^hqML?&n4Dz*-_2o zzPv-S)#lD_0&)Nena-w5IhSp~j}5P3JK=?mL64lyMZx^GvIR7{{rE787zfq59@KD> zY2xFGg@6D1lbnWh-cmXti!FnDRKU&MBKQy+_u5eiNH`zz*bHfZEP^!pj`5f!rkr>- zdU)di+`_946#j)$Rh1&2DG53MY?5lZT8Yy-o{@J>zu`18in9cL&5?EpBs-cKMNO&0 zFF(r7I;(3q{I@&ydz9y2RlIFnX8AnmFyvDQ*L0HGy)BA%P?d3p7;~T@T~&+QIrpTo zrAV6dQHwyH`Fn}N1DUxBa025d;SmH#=O@*A=~)4$T4PGHTe-ui|(p?&6_ zItP2PmHvmM{*R-@t@sfrLF@?Ja{w|kGTO+DhO9H3_4GvC7JzzEI$W7>{-TLu1HK_v z0!^rHh;2j4fs}FIs9?^iVx+K z24?(!*kuXi`f95=`7tV69Q;Eirs*ak_067POQC;xFdp8UhCf5qB=i``fniS$Y?_4R zis;J((}8Hz1Gq|syT>e1OdC8jb{pIx2H!laa#AUU)33i6N(S((`b>|iJDij+@1byupkzC*_js$SRV`H^~H;e@4*B({C8D zqxv{+kb=*N?+lILp>@{Ln8iEvjMA!#npAHU!qLc^v|;`(^l_Cvfc_lP%-dtHT%|=@`&B(j+t;|kLI^e_ z`6x?JLrZ_jLgZFDb$ds8V)nL~xiKF_St@4zOpP0QOyF#9;E7ld)7=5NFGw;gP6D3r zoE)4#F{f&~RWJ!IW+JD2l1QYV_wrLLp;D62*2arVh{A7@trhccRqBNTQKwI3x_xGK zZv!W#WGS2gdnn-gZODJaJg8uqIN%dB@OVG$NQi8XIA=3^v|U6L_kD;vZET@>3wcCyS7Ix^96)p5{T&vM zaKkemr+ca9XKfFl4t5@vOhL5=&-jgGeNQ$A z0&>t4h>ER-(Tl?YJD(4Iv-)t@t5d;}Z*$}q$=nRr{N3H5TrvV!tFuuvwQ`P^7mFPD z{@YJ5E?CB+z7DUQ%GSWHBEqet4vGuC;XaZZ}o-$l`y@t>r^jl zQoZ%T;CJgNzW?=WdN*Ve;dr~8(BMsl{!af(6B9 z@9mUcKDRWy8MPrGcoaxN-kqJe-Z8sp^C;wzgHrn<%8AoM{=AO)cgRzrj{Xj!{P)%~ z{~u`CxQOp=KV`l5y(oVCsX?oMe0{|jYxf4_v}qC3zpfB}y1Pb`C7n=POX8E`IbY%! zw`FaF`3`)k1Kzx^GkO>{60wf9CNyf9{V%ME_@PyoQJG;1t{>q+LQ&S=FBoD7c@GF~ z9>YtK89uS&fBNHsVtUT%C3Dm9W8;E>GM$2N3;sYA=aF!!ZBgbP9ILGkwKRFHb z`*D7w(EM2IFs4&dFAtc8C(3G33%Gpt4#3QZ?;7Zst*Nbr-RP5D)KwL)lLUrCww#9h zwg_7nT`J$DH!OcUZ+0a)qH=f=>AcD36155<=2J5+)Ys8xaj*B05ADtv|0a1S{X*{y zs$%MGxKDvscGb3!@%d%UL``(tv`TR4OfVHof0`g3Eb5P*xL%cI%bRML1HPFMu#<}+ zfC4@&zJseDY3`TRNn8cNCo`NoP*viP?!x7?r;e4|{_MNG*_0$-1f_z2$+!Bs%vOk@Gv2j0#eQ=KbL)_IN5_`n^u_H!-lf_xec#BY4T?zImSzh7aqI@M?}4q=NFX=NA!JcZ=-kZs@uh^2Lq*{_*loYk~c> zEy({@MyKFkrF>f^pbGKjO-&0vXgoF8p}~Ya=$OCav+(EoT5k~Pu-WNiDC^N2D3k&k zWA1_*-2kZdlHtX=-HLjqS8qwbo>E0z|I5`#=^#1ZyJI>Zd=hxGO@A9^qyY8N(_B6H zbz1$m*qtz$1!#tr!TjDfN?4r?=Y6{U-{0*MX#EyoI00(WyBGh1ta{ox4EG?{^7LYD#`D8_ zYPmvq>oO+ozK3o$ze`L&L|EX<1?9Pj@ZYU(HyCoq!(bU#f#bnlCZvw3x>zAR$r5fC z>Bg`bfI>@qPya6BtGM6U-|v(OY7fDAA{&9Zm{#9I&aj3T2WZW@W3cm~i*nS}F)>wV zSSnW>dF-l3_og-nFM0OhwKQ2+r7V`BnXEl2S|ACG*2V~-`u#n2I!VMiby3$34IKl$ zm;4=)k(;wLL2B3Xlc;_?*Yrt)Arc9PrCaxYfa?DfB_7{W+9~{p;ChyQfH+;7@$)rb z_n1Qy+!RC#Lfm(ODk4LSA1HY=)>#Yu`jR9fve=6s4jp*(J^Qp0)3!9l7(RL$9GA%z0HQVXDj}D!)*Od*w%JiV1H_q+awb zcrE6!1&O4CJ+7hh4+&;cN&Ue4CM}n0iWsy3uA|=x5!AEwQVgnL3ueK3_}(3r7fiW2 zoB|1d6Oo#CNskjFm;#xuhh(FPD!gS`~O;55_zDU&W z#jL|Jg4)7NClsY1L-DMG+yobadU4Y~{eG-~s}-mMhGi|;Q50r`dY1cbkE2KyPY%Ed-@irx)tP%&DBX|ceYXxQ~|BuO>D_8B%;Ch9{9 z>0pW+r7D~`YZglO;MZBf9tlL2BMpVfx3sUHC6d&D1TWJIE89Wj!;f{9)70N6OcK`m zW>1kOcL|mTiNkN;2}t;KMM8?8c-V0!eB?h#cev4r+L=Fc$}ZP)p~*lQ3t!DA{jk`8 zzu2dy&V(}{p36S&@|)gRR>PL^O6o6(-JWBE`&6k9#bC|GxpDMjRIA$|@oQow5Ac35 zdskZl|B+k4LzWx&VKA8?MkYpP1&5G7D9>B4V-PrGi1rF#B%rM`#ud&?9YxVW{Vz(1 z|BgYXhg8;dOWu~M9}4WmSgar%-Q|NbQOEs=MBEKk{lyo4xSx;O7JrzW9Ne|pqS&K< zKHhlO0hLq(tHfWd4}-^72uts#3_`eq%>>#T-`H29bTUJ*U6^KJl`$Q83zB5mim zd}AkgCXqigHOTp!Zt%MGq5kZnw%7kGBDtEG9Ba|w_2P%TfhHCDQ#i3v0J?6QB* zVE>UXSC^?CVjVAm>ZET_H`xZH>(R9$4Kz%8j1XfJLfMotphF{Pg^4x&8`0!{5?mpA z9rzzl%#ugzrSRQ^M4HOKVAo-B4$}j3P~|mK!42bQu1c5&;F};uN`E0zrXv z|8X7?zsSu!Bg=aO=`(!x{lJL`-+v>wSA~gz1p`S$!_%jG>u2xV*Y7 zN=Lxl%IrU`#u}IrE@*ABPd{PmJFTRW>nD59P)WWkIwm_9S`(2sg#Q&mk=HTKPSJl8 zpm1I~)zi~!=@zeox9AbGW&kwa7$3e1mF@D$Tn!KeeU(qS&J4S~db-+Py|c1Hik=6I z#ygzQgxePA^4E!Drky5&+}AbESJkX!CnB zV$em_nse<=d=}I=AIX%@XyOsk%#KH36rj-YCZyER>=@~%qA(LX)b_6Q_0c}h-R9Knw$e^hO zd&i$J_de@oKny8m1IsFz&_et0wR6&3bqVC1uSU z315`G7PLicAc{OPxa+3Y&|3yU$3vr;YJt%msJl>-pMe>+KW_JwFuygX+4TLxR-uE~ zt2Z1LXZND<1%^X-1c?k zmwR>1is2gecLN^D0=zHAF{RuCgOOCi2y^X*7;|Yse*`K=(g0kD>0aMHR zn}6V%9n*;-$ML|w4;mlYdbpo!3jz#Fw7B8%*th{B2M(jmm8eTC4|gQcPK)TbBZs9P zlS~80T=sQw=mjOp-$m<@4)<3tH06)^?rt;0&v>E32x9jcUyq)&LHiB%nDKBIuYGoK zXofhbaWgb%gTq57G;K7&w?Usj?c(WNK10*t!wEbzXk}(D-F6!MP%-1Cu(_b$uL(~) zf?EgRP*=D;@}yVm*@FC(ALE<)EW2by_!D_mmvy3BH#@7A(py^4MwVu`f3Uy%jg1f5ev*z|UD`t00Ob^t~I z23&vISD0T{*LD3jiR%O*g*<6r&sw^Q2+d|HVITXNexqa>JTX_ZJIU0Gs=kTzKUSa3 zwK@3U!(u&a)o?=eN)bWimC{xRp(hN+m$_%%zi>-dR8Ed)8hZ&kHIQ>?6_J{1wS&OO&iD2262k7+)09M`almF* zGJLxC{?$jrM@Fc-gGaLa~HlEpQ#2er-M(# zy|@0DzM9@=(MLAXM{cZbf{%s}InN7ZkwR!pI`;^Eybc9Qb?O}mTa)H|1WrG4f7CE! z3f!stz1h*S_Kaehl?sKGpIbk0nVrvKZ4gn(3D?b#uA%(Gea(J(?oFOqP>2u%#$c52 zU75) zk#$;GjS0L4JzZTrjeIM)1cs)^x_tsYS^>Y-o=7-~V*|WiDcJ+QHAE&U#k+c8nRxgO z8JiV%VVrH>57dr(mJ!C?%1Wi6Az7<}U|jA{5lIHDJvYB^Gb)~c=NrDC^qzW^)$pv-aZAp!V-f!wnmgdXv&cplfpmmTL}h^8kau6& zYDb$~{QWze)Ta`p=W36zD(I_BC&szo5f$(;RJVfu;^x3*JHXt)z6=J`dcQ;$4T_ER6C3^kZTI0H{>B~JGH)qK6^yxse zmaCVtVdoZAHvkH|m}>YE{F<3Kuxaw?)ig~hhc|%h!kl86UN5KMwC>K}<%Tq4>Cp^| zw?6Q4BY~sAf6XV*{w(t%K$f9j0=+%~Bj`m@-TL>fXJO?!J!<|IG52DOzrM~!Xrh|z zAj1j5oU=6plEk~WODrZ!EbG|?+oo${UDJg0Qr@8C5 z26`7;=x{kWJI}BOwCTG$Jw&q>7sUwDAr7n)Hx44aV89~$aBBr_+`NzQ!L7=|vl2P` zYmyA1rZcBL7wR0{+kbddIjSqElOtk1dvwd15TO|5KuYXrqj~UYY{MGiODMndkG!R# zY-WS|fgZX&Z%Dc!e&Z?-pOf2#RKSp z^R(1pvsIXv#ZMu9^ny+wR6g+|s<}a($R;t>WVn(1XxXm)N{6}y^R+e`E(Amc%cH~O z+Y9v0O4~fwc$Tw)AgB;XKLnC+0ZAwxV$Xm&;M1m=0BjN`WyYv4S|=* zS4jF3z#Nx$KMCG4HrZgA_d=cI%4>E9x|DD=|_wY7d8)Eym5XAWQrbkfYWIAp`;c3F23f zPY)(Nq=8=_Ak$A<%%lq-h$1%R)!}{P*QhuH$(;<@yAUWiat%0vhQDI`@J(}E4DFyd z4nZq{#xD2Qi~0lIJJnosC|fOXV&zDn_!yP>!+^Xz!?#PdC2QSbDJB;oWSLqI9VMGE z&ID@gd~cz%W8t46(c~_x?c$K8{)=4MTeyamyCh>^Z86sJ6$8es(N7*sUgTbLJddHa z%R5}3FiD^deYVyYWbi^1P#RJ9J2}Qd{1^Gg9eDG-F5aW;!LPFY4`;i3oAumtt)AQr zI$IiwE|sST>u7_*7vJ=oK%6#-7wVxGq-5jq}KE=-bmrlHDzqP_L*)_yu+ z0{N>wYNvm5eee<9i>y;ElBDzhw#S(FA^XCs&u2|N{#Ss519RLli2Yr{|Nht| zO4-umMV*BzoD#9T_WChL^8`xuI-(M-r|Q(D!1GOi)p)C)vzu^#BzO#EpFQ^nRD$1} ztCfb097gjFAdvzo?#QftzN#m=`JrM1DdfznVW4V0ms}^L%$(O0~ElYT& z$0O2|-wC=fmPYg28e8pR`a&!`HD*w2z3e@g1u2$*?Vv@3-9)9|)=#Kj$kt}FSDO>r zYhI5fEOSw6dg&(v%_&7Og`Cp+cKg4$6;2n8#-!9AQ1m|b;uHe3I&(k|cXLPYh2=Gs ztY|KaDrrown5`{hOxCLJPtIOgs~%Z5ISD(`8utOBHAh2+XqLI=1vJD6*!gFn^sH;; z8T5sm;=wVg(fqdy)IwLnMs18(6%zBNKXiD(b4&~k_m4o}!b15PXP6rf7W|)Im8p3m zM?0l`H~2)9y&*I=#_)=Se9COhInx#F#eH2t9pL zg6#LsJaP*1>t*L22>8CS!h<9TgS$VDRBU^_esjaJ%N58ha{S=aD&>QcNa@_Ns^Evy zq7ok}{zYY+Ec1hkh)iK5++E@~#|0}6kEvP9KYRGpMcS@mLQU(M9(eyiXp6CP@N`kv z;=@;7IOjG=N+$yjws8R-U~G}u3pghcJ*-8Wvs*ZV5C$|F|4s@N`Z`a_-`1uH@l5l- zIk>}fFj-S2zfEjWL)$1nUL0JVp>5Q z=ao4-z954R37LKK)_Z7Z(0g3$J4_u#efW~~lAH+{^ih5ix$X{OFUl4t8Z z6$kIBL1M%s@^FQG+V2Mq^BvK>L|~Kzk~D~!V$p$8jnvxcd1UO-QQk3MmlJv1f5;8F zC6xLV2@a}VA_lkLvX5lNxsZq_W4`Z;8iItgjpUHdKV%yQO%lpO$&P(;9ct@ApE z7VtTMdQGt?J>@RHPwW02PyrEs+d766Lfq+3{TrYb^B{MR7%Fd&tKKCbQ1PYb0?PTf zx65=@2DT{Mq<=&X|AftOez`v3qWVqsjB4`BhNO1_C7pqsKBkmdIg=y8@t3Ou&3d95 zBzeBLsq0D%Th!P}$r2K^d)G3*9xjd3q94HgVv&uJ-eO)wdn0Szy>Ft|Pb%c1EqUs$0UyC1iBK*RXgC*@ z9v+Y?0-o!(!`~2?KVE$~Z=XPVWHA#Cesh4FD1EG98hT&hKU8`~{Zp>VFbx_?) zL{vobOI(Lq9s{47If{Nx_UVww0Q!zO!F%y9p-G(3hZW5-n6i&T0z7Uh=Rv7-oi6`y zBH#8iH@6ds)J#-i*Y1k_ye>VEPBkg}-PD(Atr32EeR?IMWbGr#AP~JMvS(zw=CF2V zyx+1H=?>EKAyEq~M7Vlq{`)4rFkkDH(sn0ew)SZk>Bi|RLF@XH-smj)0w+~J4y&Ez zb`G`$#I5Qz745~NOYoHbU~k^qOSG(~u-PPoNW9M5d#>nFfgp@WiiZvj7+>_ithjcD zCjXNz7ccn7kbjsSlnB8jZvf`g)FK3PyKwq(P>;d&t|J;Lqs{&LdNat3 zKQ^lhw+|mgfZaQ>S^RNk?=*U(pJ0wv>?mJy)1GXm3G6kB@KB_dA^GtG^T-#52oAk3 zv_}J)^C|4U>?M*W1G$;CYiYZpZXYKC2EJ2&x3WwaxXge9c5fAQEkCp(6BydZ;a~V& z(el~<9LM3ZL3Y2G)l*L$Q05@*DTvT7#MDcwH^yG7ZtICZ42Sc4Aii34s$ZXq_$sGy z$ZP9A69Kr14n&9Xw$tEw=#VGVC-mBj_|u7HaY>m+o;b>7*bfRpiJ7B820prsTLoU_ ztIj&Amkk{QFwDf>ElIcja#otlCjzf^WKQPBZNfIaVsltx6%fIY*?KJz2M$Z;rI#qu#74v}-y5F{dOKf5xo zkL^rp{~oQugVw(p8`+hQqpQv5${24L#>b5sM+4B?FbG^VSut5{B23Am(Qls6298|> zKc`+Qs)SJ4#_!>MEIOadbb32JE!ebno4_=KoEs&stPq+tJRkUsG)pL0$hH*HUAu9j z==+}{9Q{66hy5P9aFzOz`bjH9@spB(o6$^qu4Q9odrxnrOnzq!me02${gFZU&tk*Q zb;^Je>v$yQ=jgfg)n@|)Oj`r-=_Ja5$vW9V9NEz#^Gx`cme7QMBEC&6wYzW=AEE_@1d(zaNx?oWkG7cwM^9RzyQN}A`sYRm2NdI%eTe`B z5tyqUw;F9}O*Q%RC)m!x|7(u()IcpM1;ujRpu*nnt88uTl;`58d?$DN?raMyWgg#N z5?~n6M5*ERmAuWMv-c$UiX~ny1b?LeA9dc|K)nhsUbU_RzVd}T<5PZ^;~x!0Ho9ev z6SK6=-zrG!!wIsXywF@T^&DT?90%U@66bT#`c!2?SqsD?Qd`fFP!rQ zNxVYLowp_-v2D3;5Y975Au7eWIyS!_8n4L)OOS`)!bc02WRes(dacgk#Jlu;*WM}`jQ0;ZYaK8 zd`0Y1k`=|;v8}0fJ%k;vQPqNYQXzs>GHpJ@bUIqIwd}j>4$K0u6N~vle>&qXVC0f5 zy>c~_JUOASg|gvvIcUSZg>PlmrT3Pu8gLTSZmB-8mo$~_bPw{@6CfDwA$fAl@fF7n z85dd7Lxi8V#vrO-!W;3k=g&J!14j@tG_R>g2v#c;Jw*J&vpDLo48b+u4}>4ol6>i3 z1uF3+4IVyxMK=Z%_#1TJI5a0wwMG;I!6B;nl0xh>?`k zvlIW`71#En>*SkaOU76g2-J@TxG@_=X`x5g-A7Y&i~_#lFaykO&%NHonh8Wc!$O~H zCiTY}m^zRuI-Ft){n>HbKl~P!{im<%Ibgn7QsQg|EaRkzy3zuT2LSQ#?e?=7M)Wm+ zSO?zHN`F47S|vxJdyqp|SDRh1~`!3(ZA+LAupKs?8%wVc^wYgI${k%JoZ8 zKEiDKFiP+omaMG8#ZxCfx9zTRe;_NgN&MiXHa9Dy?~& zia2JM%te&(VPiYde0*v&kr(s15H~p#@8#|vBf12_Gg6L%nl-2XZH5J}p%J)Mx&KL~ zop9DtjRZJIR5Gk4iDVkI<_0nHbKRodxMn$%6woG=)MK2Hg9o@;lV;Lf2HwrGH~$2w z=Uk<3CF1>7!(TlHR3Yv!1q%>$NXg2LzYwHq9OCBk>v9^%Zw2e=Lh^(yvs)^0d_ z2a;;e9F@>$$3n`ol}J)cY$ybgWkdN%xh8yAlpAJ4;0$JzK2_1H*BK7tVu_|iAQ45X zk82g(dtHr`b83v(mbTiXme_F3M;F%Me}CN&_bEp3uz{^6cXSF)t6-MLLgz3o(4-&` z0VrcKud^2~N%+dC$5lviiDrKvuT!t7q|7flr=}!j)Q^lg$(|ARvBofB4sf^$UfBNj}JL|(-^l65)wjOGs#EfpvA#)Li8CW zTf7oO^d$~2@xksg>qX66Y<58;K{PGG0QeucJ-so&Dx2qzcSlYGtw?GEt-%Ae-Gesg z&kdaewi!_im!_tM!MbJvPj;3cXm*q&w(Ro33fjM@Fx2Ni7^UNxR*x3I2l7#hi2vB^ zm05t!rV)$ivw;M)HIJ+njq_C`_4MX?vzUhA7QX(sjkC#(z${DtVXPv{(p?Jpo*0{U zhy70vmwE?ME@c)IRLDp<<}LaA>%EN=58=J@^7^$5Q;zK)c*d9)Q819}vKN8*^ibp7 zmiZO6JEW?z5r&H5e;T;cD*c~IH-eEoU@Qw#97k!4EM3&vlo<4itX|#MJRaN*MhRPK zL){$P6PCq0{Y-6u#e@acq@knwa1aNMLuHx-Nj64Su;f7HK7^u%R%~~z{Uf( z1av?^4;G{i65zdkCZ1pk3Aw~Mm__x86+e~9>I4lei+8;owO#hSJ<~sd_IJQB8tgG; z(s5|kdVij|H%ExT1bM>6CdfbU#Cu4XH?(9RDtL?eQu-wfiHw7Jexdx`NHH8Y#{ATk9xFSzKXR(VxR*E^@+2jz`R47*x3m zT5SeZZL>4;j23rEkX#PLgdBh*=JJC`~wG93QGT=bi(M5XSoRe59; zc~TYP{nE4WUZNcqAp@&ci@@b21;J^Lxc5CIzYwgJZE?s`ibC(w{F{W5;033pCl&0^*@t66!niCG(Lo9S zp zD<{YR<*EPyJ;`P|(ZE0spph6p7_+`#lZJ4WJ(7|1{bIn{BZk`_ELOe9vbN!(d)JzO zT|C~A{Kmj(7U?cb@Iv7HSI=}>txaE6uy=BQpuZUHz-%DOdQ3KvjS!pTV{l;&8w>%o zg6>`xM$KZIWNK!5yZPXHFvV-`vh4F`L6|NV6u*>3qM1o9sU4YMz!0NQz>pDIy(|NT zsy>k18jGpd38r#eTnVWDw)nX6dF1L+VoU7nV3R^{L@Z{8GYgm@yf5M^N8aT|hgM|L zUQovHu98d-rs;mtoO*Rz-N)hQBIp1 z9X2=gDYqMonyu*!)2gKo>T$9-JHe7^mb883a< zjP1362r4wv_?P7T*MHb4ERDM`Z^1h&A3>j-We9v=ZU|wN+s)r+@*JDTIG8rX2Z(73 z0+NO~QUg%@im~X@H?A`oM1z4B+vg{tuZ{}7Oht$^AMUI~H z=AIVTnD{xD8Na4a1|7?`=c@klbsd%!--@`tLTO_$6K@Sp!(vrk;i5?`5fL51h)=%{ zYq`tLywX(A61N3p5#X8Rex%@$_M=0|fZq+b@Bmw302WuN+4CFZA;PWj12{?=%{h%w z)kCX8g!D`J->W}IKBs&WsKFLaWlI)Z8W`s1Mw4l4?n$u0qqoGTnOnSycSaj&rpy&9 z!mkC*5W&}ij_h?vn6M@ys#+1FRcBT6_8O?(Ukg15cQtZ=8vzK;$TV(REImys)%GKSkXS|P`UDn+G&^5b14F{?j z8$J7zUHA$R4EqGI^Hnu9#YlvOgl=9o2yNqHZL!{S({$yuPo>D=Lsb|$!tez&ifXJt zwrNU)2Lapmg-s{XZCIwt=zP|rnFxgGo*oL=qWm3sCekVVeKpgMRTA}wWL#k+(Hh^rD(T!f-eDr)?|gkuvRQ*?UZp9r67b(5^Cb(VxmQEA4aTB zhF|+@fe)=kg1J_n%Y^m$pHg223gG&gwECVLYgAV9Sk_3(u*!}50yR|-I}i1~OwC3o_kZT%TBze==Ke zEfWKx`J?LFkaWn!o%nwz6(*SV5DSD+b7>UwJjms3kR*ikOc=7ICIR7|2@jvvGreGC zCr@Vd|7N^lmDg;SP(;S67ig}!R<35|>N-DSGmWO zjiOh`nxChdRDl)9@ovQ-mSKG|z-MGa)tW)ksZua`a{BYl>!#rUjvN7TY4Exok$&DA z06exIL)Iw;o-g%iA;|L3`?b3q>(r4P?f=g9yAhfK&Xq3u$_W^izF{Lpr`QLas2Lco zDQn|?5wnPr&}4h%ttF|=+5O~!k2UvmhnR#}=Qg09d(P-U@)fPMb2N^2#PbS?hR=|V zfMI|n4TUHAgyRRSWDr1Y9b4AN^{9tPyT{@>JGjaPaiuIQzYZXvXEgJ+PL(-&Xm>rqXFItU0WY5j(Bv7AeU|6`eOrhm(Z6l=v^>Z0Th>PPwSFX zbBJ(r^=!zXp?@Modoj6M`$A~tRvy2tVyFyM7o@JLiSpuAFA@MKOOU?}>|LnxPjs}} z#jCEcXJ)f8O_uXnTg5y#pHAef=OV6`DUw$gL6R~k>V=HlaiRarrQX5+a=i>hS^;En z-vc()fVe-47bJt}yDvgz70LT1*PM;**B5s}Zm`uttG*vsu|m-oF>0kIZm`}&%!=x8 zi({6JPM`X21Mo=SOHgCyqY0JuA!VcW^a;2dmr%j^nNIuiXQ@j2F)nDkg2G+p(cIZ0 zbVXAt`$LHRG)k-*CH-=_RHut%$LeG;oA8e9^_o`JsQIi8sY`R_LEF1RoptfN_Ecsg zH`T%`}kDV zsKfr4gCM-V1!_V3gZ|z1m@Pe<5!8g>GzBR~u$|diVrucl!&ZxT{b4k?Mx7S)&C}x8 z3!XdKFU3n@o#@lIf9@9c@Pl#AfomK8c~6~%9%0<8Tg;J~V${qvr07v79isW#ZB2)A z%V^h~tvY&CId8qy1HN7|GktRrDPW@qD&;of(%n+y7=FS$si~FW2Rmz5r5VW(PJQzoCiNB116}R|^pK9zlynGx0dzMX*lOkqa!QHeg_#R_Z~iE@ zK^3cW2}5=Uw%!3n={HrJRvE-!mU&a6P0N){gZ7&v!rKXKb5_27YbrEa3NZleZF^vU zrE7QxF(Y5({f7hi55Qy3fbf~YRLX(ZxA^yf*GT_JRj~U_K`JEK!}ld$$>vg{dKM>X zcOW@z(;$S*{|EBJ{tn%|kCOL)W|MQzEELUKCS(4(j652}_Q+JY)56i^QLBM58*2-rtuQ@-Kqe zKiVbVGQwc4Cp6ldn)uYig~WlOfjR_cU-9tAi_OpP<)+QF%#4$a%Z;*(jg!&ELO$Qs ztHRD|KOvkH{|aHLu&=bnR??-Cl#XmV>nrS7-`Jp9H3z4UG9W;J`F{u>2JFZ}Z-U#i zWt3A6FXkPK91`tI|MAKtD!}zvGdqOR$`mfM@#nZluOPdbtzA!9!!uet09?cE7+ zL;W2H-IQJLd=*+>noBMVcXpQ#>-$UjLAMFRF&>Im(%YsqQDK$Cy(IRy@3z$j&abwy zBh9fyNB);WLVWo@!1xMd{hUX69*(2HLh7o#H!4tfADZx(i7DCf2a@%%(i397>nzpJ zzY19RQOLIyMFnuwE7c1MZ2i9Sf7i)$HRE%JT$L1JrWZm6U3D%Amv}fO*pe%A|D^D_@F+MO6%3-rd_ z7=cx$#bBQDe&|+Pad4!i|Cn^p>**ST2P(m0FO&)ThbKRR_T(fV@slo`1Cn%tEl%iQj>G865^hfi2Vu_0$I z$7U2Y_--}3sAgMRe(4xdw2`@y6nGN*Z2c<-_p27l^F2d=wQ zMmeRB9EAy+6NYUQ)ToZkgJ>L#cOIQob@is7JT3kC6C)-5Gqm=4>ZDBOSe@ia^Q7fo zFP+VMYC*1H#S)xQnb5~ti2RgwG+mB^VkS*YT0o1$m&{w!Cq{k*izs9t!r~*V3rJyo zcJ9i?W=05qqv4Rr%Ij2TnQHi25dP2b`vzMMqwBtqRoN?EY%Y66q)HqD=Nh@A z3?++Hp_zhAjPPj<$t}>8e)H`-L}s(5jB{sr3&6rV6@#Eza*(>B8SsTl{mqDdR-P*{ z{7Euk9}68?t)I@ZCOJKCXU}P8q)Jvm2avGIAxlz9V<>!#=-k{m%v~cYl_T}Q!Ki>> zDk4&~nA1ia=SSCw(4;{!m*!!pVKTxDLP~m6j5#f4kf{k9Z#L z-*-3PmAqH-RaJLOEm;PV$z*1C<(&<-)Q9Ts>N<5*wNW<8KkQpKuU*S=gTMVDp9=rX z`&+5h>dAL}ZkqK*^>6IvZ(rau4I?Xn?mzb+|3yC4HXB9W$Xz#^W~)`Lw9KYaF{{;1 zvsLJl!_J6%uZT*ckzU2P5LO+VU zp*yeKWAFb(_rFol|APLf=zqiLv@6w4t6k9l|1A1%oO{v5v|s*1_W#Gx|EAe2=zl@~ z3;JKs|Kjtd>HoDKTn>i*by)t4`~Rcpf3;Gr)(iSy(Eoz|7xcgQ{Au*x_Ke%n@U!gy zX1zX_{?}Ty4E;B2^~T>=DrVvKMGzc3a|$DxijsIoq^usM1YM(!l!g>*Iz-EfUrcI5eE zz^x0{zVxTj4_8(39-3KWfP>5)`eT>ry5mj&B9mQ25wub+bNn=He^d^!96{kHi_eRFSX>%T>LQ0b=u zK)G{tc(t`(^;^|(zq`40{c||D9*17{=w^M2zXa?HVa`hNKHe_xbMcXy}rF4!HmUv6ETzw?iN`*B!3f7u-T<3;%2 zH}>m;@zufMFZOTU^(RvAg8vu%zu^A`{|EjL6tCOcd)eFE+ueG*d1Q>7FT?+v^;TB@ zZ`JFC{{KZj3vAOLM}gO$Mt%U_uiUXUj^K;qGJk+Cov9s_N(&2YZyK=uq3fKxrPB78 zMHepf#*rJK?*J4X_#h4h-uRqZW9CoXG5mGdwL1h+0iOcP_5y!Aa>vmsJZ4r{vXibu zDh({3?D#JHo={gT^5N+vHZZY*$g{l(i3rd&^8uhq;JSb_x>{p$|Bb7}?v4c(h5lu~;t`PgDatr_v^97ga8MiQ1v-5YBJGe`sKyB8qX zLqpMp1%jQNba)AqYX>g0f~&?Re&|Ksm8&il2!-}=>bPsA(sTAU#Dd9^;*jZmh_$3N z2L)fa_!_2S0&6pv(lj{UV8HR?%5$$LKCE;oCr!*x2vuL<1VEJ`j1xq@3Bd+1hfx5C zi2Z;7iFgo>p!7@Mo)S=i5!X06FNEO&LJnxSZh{X``0&ENCQKXw4qe>B=&y0Im=6V2 z%o(|(J}gao5~pJhj@2K!$&B`I6STzfsIzqh-A_Z>MtlVUo1A!-aACa>dmVJqh?N9qd&!)G5cV$JcvT5Y&@PZ~7Cn;Qt%XwowEh7Y6Wgwkjz=lQ~y0{Ig z58PlwSbe}F&jvZ#avaj9n91;nykQT4I|3d6gwF9I zBtFnRHI$$ge)ISj$HnEm^r$pEfmNKk0`ugi62u0zx)RYJoDNX-80?4hv6zty*tQv# zLzEBR^?{p2uw(rIW**iaKthDE#sCe{i195`*a$5W?^@1A$jKf&yJe2+j?(zD3P2$R zbjP%%e4tTdq-NAT?1d4KB2HirZD4Ap(oqhqxSB)2Bj6gKDH#R-9`q}VyE^LKrFgpnQ;0axwo>IopZ<(s=`BQke>&!ChM0?Ok63bd^*q`EDE zz)~G|=wBObV>kq{2-egh{&?*The+-M7n!CQr8m9;7Uu!M7Z0cu%olJ2qJJ!*8G!mF zKq*9gC_Hx2h++wrqtYPSg+Nxlz@Co8j2OhiB+QX(!QKf=J|HmfnnW``5FGD-3n(Pq zaA#63SIM@;^aM1~i1Rj8FiJ1um4v2-v=Asbpg9Ock$A0$f!Vmt_*B3r>d?Gpn;GO<8F(=+IL6+UlFrIVYcS=B%$tCs{$TJodWayUor6fW)E*EBv zm`#Xy592_VHs+W>$WszBMFtU;VEM`L0z7uYky8%fO)>x!CJtfp4`Xv1`}SMK`1Zp& zZdo+RefJ#{eM2rZ&_LjWu=@Y~A1KVdYK#$oWM?XL1Md=Q*%y{O4D~&L6Wd2&?J`vo z!P|&pdOn91!XwMECb%+#z#q|YkMop_(_?h-nScm%y2?Nn$N6KXQC}@tSI{YKF(4Di zXn9;KmlZ0n+oB1>56;V?YS}E;Pmjfq(^4sEeT_ZRL@BQJXi>Gd^1p$Fq_F=L_P@gZ zSJ?lCPI+g0v%7oP{bTn31NOgarQXWy|3;bZn-|VY_G6t#jNV^Pc0*F zfLs+T>B!(gqH#gAIS(w9?N=F@*FXjU5h6hAnNU@^^?~_)6lAzo0!q{^Dl4S&h5jJA zMtzW^(U4RMlp7o$gwR;{A9$$XETbHu9f}&-3U%VRs47x^l@u(-w{G(}QcEmBHwIAo5T_EABINNWU% zp2XhFFhQ0*dfjD*doPcE-Z<#8?L)SIu=msUR(FeO8;9^)TV+3QAHCjtd&Ho`!N%^< zuWaun+t~e;{kXlmwaU7`>>qRw582*9Y5UFo&UP1`ZSQXGyxrQ~eZ^itz1_VdwzK_a z`v@96+GE(EXllEAh>gAJ9&EmbpBpc>ceamyT`j%bKH9}LU+x{S4Yt2=aJ0Soc4y;& z?Y}+P-#hF=_gm2H?)L7>1L&purn`G&K(FwOb$@~%?C|x*&JK1}+IS1lA0T{eb8r9G zgY8$ZkJ#(IovkiBe9;BSHeT#>c~>x~&7F+%Gd)JU-Bq@6uziRV^73Hs&1wm!6Kd>HBT#R* z%Ui*zW~mi{BKZ65VK;7&ZFM(xpsho!j6;-#jY1Uocl{LdePP}fijmBo}8FnutI zIJGc~6X?&OvsO8=KY3O>t#US}69*3+{{DCQ6KlConOq!)zWIddpu80lFj!r5Odt?n zSfOXj2NKBKzETLrbRAD>SoX6(2RI?>ND3kgybmSf&$L!Gsz!z7v?dCKR816v?e#xp zgRhco`QhbO<^f%J2cUj)J?+y#Pg>&u{SU_nKHXv{4}hL6%$G%J)$w6dIz6YhwA_S@Bc?keGC{GTK@0<{(sS+G8y857ahA&=MtFl z6pyiB)NeyR!^cqH!gI%LYcPlE4^EPCoiGBNR+y%dgHuw98hO*ViN}lQ)4-AkPp;?? zP#%vo<&mH*%AvjFBJii@7a0?Y;@ZTnV6AaY(OU9>{Um;|IiC;g<#f!wGXAK~2LMBV z_`@D8@w-0+#@W%E{jKeTv-z{}Ga3PvGf{4)!_5Ng1`A+(j@dXnc>Chl-ix=}J6qyk zj3PLj0fX;x-n}Fb;&2@CBr@3Y7Dv=IL_<(`V4IC5czQhT1G{!@a-IT&mjhp!2gc^c z=IibpFp&IMK*LlRCYF7P{$4acDja7qO{Y3%%p2g*zJG~;hbd;XG7HA*qoe)a=FWC^ z_vkDEV~_h9oFOh(?Xy*OW>16R83{#a*XM3@hA^U68ty=akg9+UyZ~J~$5?2}-Bxh8 zD|xfAaj?Bn?d|O!9jddj;cz=7`hiV2<3W?B71Cb`-7E~Bj2oM84|Xz8JPrm;&F=tW z7L1=?b&oz848AO%6O1?82M2oxJ(v@9Q3OHdkuVHilJI1FUh z=GlXwvV9?_kXw$F<$^y1NjG2aY`i)=%LPN3jWbzJ?%m}cqUmZ6!Sq2e3P2@gXZl|4 zY{mFEFgEvgUv9s8d(h3&j65)maxC1$uzVgK`)1?E?%6|&B2e@UmHt@KPUhk4oL;OL zzaI=S5oP`>H7n`a;Glf2X_x_Hd-w2YV`nF4U1q^ZOc0+590f(^BFN*o!Iq_0sy`l* ziAK?ep0ZX67|O~#h@z=Q`Q82JgOR&3Pgs=S3(g0jmTW*P8=RkZOLNvR+2%2bGiIq__qxI00o|g<)dIa z7z|x}xFSYD9@$b!rfyM?C<#D?Kf5K|^3D#AwzhZA))E_tB}Zm4w1o{_4n~e4go9@q z9iygC>eUYWmX(odlp~*5lff&d+vJs~Z-D5NnxJlv6T!MPV1p4!h!s-qEx zL`)>`E?V1cP@D(ejcEuFyql`{`r%O+V$&8Vq-dK6rz7^08^oc?u`(Zf*CUIDQYd?4 z6jT^W@f)638%=x#E9(zsr|3VSm_pE(w`5I$-dYr_F(l<%>hXuKH>%u7f`DLf5vUG% z+6Ko08Pbgt1JhP2$uV$j;9CO&LI7KZ%Pm(vtg(H8uJgh9;7?|(^R*cHR{alV9R`&Nr2X9FJf=zYgBS6r^bP^9QOidKYXMol@(wQaYxC`zf z49m1JvfjgBHi!Pyc^UZVV-8)w;sex?l<7R^K^blc$P`AZ9R*`F1nA1f5hCiEob8?j zNfRcorqR%tCJnFj5W|4TpEN0AMCb1pzQq;(PMfiik&8E1%%XtE7ne`D8;6_%+SrS^ znn0Pid{$Z>`w^!6#fn}igQU1D4&_!>*){31XQDz}NMfA~t-=X6*mhLnu#w*g0Ky$r z5(}M*Ke0n_EIFfTkVm=<)=GM+@U;~RpKVk^1t#)^{YDfO%Zv{0(4%}BOY>exAvM!U zw8}gKfR$h`g~@Qx_BdX`6AhS8Y+~$-{i&WzkY+!$r)0fSr{f!pNYUr38_=W` z{K5)d8+PC6vfN#L@q`q{6-i%yblr(WM$vR^^sxpQl;EXKg#=Gm_%~1el9MMNW{RC6_n^iW)&Z4*1bfnVSe5`w~NyFteQ@ zi5Qo75K%m0>ga)!IfEhkhZhvhr=nm?1B4S!>I#;Qt#E`i7Lz*Co$p9a1X z`2N}*PD&+lMi9Efx}BoHm)_|y{mTz1{NXe<=HL*0iur=>iLD8-0G4bCr4Mv)#89vB z8UtWBEyK1}WvDa{o|CVR`L8GdTIdF+$EQZ=1%Nf>Hb%)tiJVPw841Fnf3E2210Y>c z9$_gld6K&J!0{jcizQXF{n}HG3(JTl<&lN)-{q`<0$Zd>dsZtz90{`0wiq^VjhIs?GX;nT7w?7xDjU<=p>U0mY}d{`Wu4|Euqf z%YWGZ|G4`f8?{z(|6}3*ThRZ)|F`&jN%~LnuP!vVFGK&$daGq-?|(GSBL45o`2T)I z?wz>ysI&Jiv}l$V*x?j(_TY9+ddp@V!MJj*sCf$|;bpm&v8f;HgAaOm3V$yrSEuY- z(ZY97UVI)dpQxicn(pS-(}y3pi{{({y6BoE| zBsH)j8ql%%qyhZJ{bgfkO)5Ts;ymeu{K(10tS4yCL4{t#_L~|2@CpuC>P-; z@oVHefVz{w8%M(JfZL=1^k~YxAtJ}0a_ovxDH#AJ7gK#UGh5@OFY}vFNM)Qmi&2uN z_>e~3VNK}*~Zg`Y;u z_@B@L*3c(!x#`3xg%^Odup!uZIviqN;1W&%Z@NeBuU-N!~_XrIZ+bW+Q%>sQ)BB) z({*V_hh_K%r*DjzlkE#%xT~jsA)QK|5GhK2rpQS07`J`$iy!R9KgCu`Js=7T4{lt0 zt{Zx3;Nd8D(6mo;kIqAV?nWw{Y{Qa!1r!t={nbXG*osyv5n3*eMiC6#cnN zUhW-icExu(QtJM)eR#CJ`$|0fvD@7jUw`Tzyx2RGg7kr&g@A)GPo0!9G;!aQqig)(E)DzFND|K=Mk zGo&(zYqCsc^~Bv@cj6W;L>uKR)ArCgBh7*3wOwfr%sfQ=BufA}9s>;?8(OMCbW~r; zi{v1tT?Qt;M8qA*_blTUjomZzyT9{$N)daM0q?{E#`batt>S%73j=C_HlwUgE5A;- zl9UUnTxyrf@+s`d*1M#7xQ%6TLB#K}?D}48knHpIxGa^DELl@JxuQ-4IgtGbtX$Xh zE4A!|$`U1!C1Z?*vXfN1d>v%DS)!5<98u+Y3O7wA*NtAfp=EQLTtXsxx3F>W>MaKO zbLK?p6c=(Q||>7Lq&;cF@p&nIufA)RHO4f zDMH4~ODE$cafd^=zI-NlFO5;o|4NBh z; zX4wF%ZVZWoYCi%C)knAql71H!o|j=8h#1KXg_P{0ym+U!0OheS2Pc77ycgm#|HWze z;kt<<=VT%eU`F&Gb92I6On9zi^8b`N#dM6htf38TKoFLC=Y6?2HCbIb$NkNVEk) z5qTU!CCu$RncT+G1&L%JVsj}Q5NSp+M~2}>0O;`Ax(#_O3Mu$XfQk|iTuLS@S87O9 zj~P`b@zq$*t310y7zwr@2mlS@13(&(8-SIy6;K@2sO#bT6JALCN0S`+CE30ABOA#80EZn#4UFZok^zJ)#XtL4ULE zW`Mqaz!8>qwJ=4YrS0%&N z_IcrCXrWzggt8AReK;E<%pQ!=+L*sJ*SGTp2*hN9DW#?`FnHuRJS1znAY5@|3ijCgeG)=20pzI5xTo5uTl#DS#~D ztmBaBYUs3vndr&%FGh>&^?gC#H9DmZz>R2~{-R7>;>6ZS0MG0<&7h zROV(-D7!L!urFYiHTKmyPA`66SX`#`47x7l4rP0;+-K*POs9bGYbWEAae9W>6SF>u z5e^lh1mU=+t7$)~KanOv)Tdk!vhmQ(G+@Z;v|RTjlW-aw6P=8Lkxz57qVxkK=U_S( zUXps?URdG9nb0gOHkbC~?5T=6Cu(ygF-$>gXgI>`CF+-o^Z(-fzc~Ld&j0hy|EJ^6 z>H+YG|5wedSF`8;b$DHz|Nj-v|KE<2lr-_J463p*d-5Nn4?oN2_J`ETKR*T52Mq5M zz}!8_)iXQ-AXaOnR3o$Ya+8NV&!tb+{2?u@aosq(5Gg&Y;x34!0Y?hEpcYY>i4b!Q zJ`-*n(IDkCo~=O%Vv#4Y$ex_r+_}GqY;VHP_ZQlx9V)t`S$da{r`Waao zwhK{21(zQRC7{ZKAp%4>%u2U3$Z1KOgCtF*&O*WqZxB(dSZ6_lY5gtNV4t!=!zhHc zNL7ElaY;jYUFRnT>GOhbzA2HR=%Wm}@!Wo1lkVfq{((l_xbx3x*8Tie)mbBD+J5;J zS0|*xJJ(JrNd_o?Y8OjHKH}Y-*e$tjkz3nJiWyThCrFy;_j`7HyL-yCQm*wfW4p{+ zE(sm6mru5IBwE$%G|X5lGWtYL$4^yv3K^^L|1R{ue;xe~#c%Bs zVwCD`Jjb(m#zFY^xd(rmb8sD6@g`SwIf?@}^oLjc?0^by@54)R!=^|`#A`Nr&E`Wj zo4GZ!Nq#eq=-HK+@qN$ne9!m0%lChN(EGWhoPNe9f7MFWtkfzUEvu1mb-7z2{kHb) z->Yna(Pf~)m6A6E<{NZ%ydQAjuo~zTL$5FNqm<91c*dCUa$H~L?qRVvlYAijtAg{y zInr@&xUT_e`|ySY-yfutq;hg{2XEIZYvw)IVzWmT>2l@+CcQpMtL}qJbD1hNA3#wtRhk2} zxV+5!{n}hvQQnh+5ZiIQQqH&%x3iSHp6C)rB>NN9MU(fXQatM>KP}Pq#g}h)H-&eP zGV!sFj|xy+L{SdohwMZ6?y zV^8biolwH*jEpzEtPTm9z|xv>LNBHrH{Ug8e4u^+RvL?KO}vP1KbIR+9K1aXHNHC7 z4zoag-_ZYoH9`w%)kEdcWm*f%$Nz*HP_d$S4E?m+E1!SE!g89g zrxwsdFMyHV07{4Ql$VY|WozMe4=HSZO9D}sWg&eM_;?-bF09lFn|=Nhr`0Q(;tLh9 zvK!fDKhz~m>Mjx7H&aCIobRmIaI(EsQuZ=OGoOz8F(DhZb7#Bb$PZTo*SWBw)q(5y zfwjt0tKY{^P*KP2d)Bxoc}&VvB;G%%O?&5uYx?5ylspFXFj%=yR?Z81Bg>BcIJBhL z3X$e5E4cO~4Gi398jQ)zbIcYO$mm!}Oqa`kcq8Fj(TGE^Q|tsdWydgqTYkqn6%&4% z6{ABx%}S-if14Hl+pLt7W>R}zr(E#x6Pcue-`AJf*_;JEBV%UTQYU5vh%j5cgG%VY z`y4=EUyI-7={*Cw3?LKB?hPnRNdR$|xE#pM1lva=cFtaIZ0~fp)M;nSI3eo3SP{X3 zi_3D7lyxde2ZXO!Fv5E?VBHdzG2oNcTy&ScZIAJ&fM0TsXWf`Cj#BIR!6kX#k*pZ8 zbfcw&d!zqS53m8m%pM*@+yVeQd#|XVmSN8E@M|@u9nf{#Atv29=Lwh=?_@=VN5|wv zaZ-@L?^qyg0Rmv&9=u^CTuVw|q!`j0c&z}GW_7W@Lah{AB4G_@hLpl4d@G2(nXnM- zzDIYsWW0vZ9lQfa$uCNqN2pFprh~p4ovFSCrlr#N2!hNMu4Q1=` zjXR!>diG=r?Ep(k4Gi8ShE;VtlXaDEXdo5OUmR|U{-Fkr7RZeq(PUp-8)ImE&S>K( zg2U-}?=aoQIgluSJYc)~2YZ{{!^6FU!`|NR&hBGPJx@1t$7DmjnU+3$NC{Rv(nz=2 zKZWx-Y=JU0KrOHMx3nR?;X_~Cj`Ag^AkR0>t(8|19%eW1SSjgb|v3yz2X-LX*6TrOat~fg)x?)bU8BPT^EE`6$RHqrRtUPnLyxED3<(WSxt5X^^lVf8tQX zF&`wBBhGk}PH#pm%^;^R%lQ$s>F1z7Yj!)NHQ-5l25hKWO{!^ z%gAAhkVloCF|N)e0<*}yJEtoF9xp>8sYq(BUm^EMyFJRLa%nl4&SVMsH!G1QBSS5j z%4p0v4>4ewvI3_m^&^7?{fuQs8|M~&%G8RIPnp(aUgt4nx%MNc@$k|^fjPk88~wMB zSiNL4vK%z=Dbohko-${m*yourK<%DP9@`UH!P7dvd?|1;&7o4&udHBQL;vp0*WJw@ z58uA&9lqV)-#a)00+?;)UXjq3 ztECiHu8DlVB2=+>e)8bq;s?)u9lv;RsvePY-5XRETqka9T|$pco|_?#H}rp;vSUks zf667?I1(#yN|LR|nRkU_kr<0`;pJX6Zc2_mO|i~IUcFnGf$d&Mu9cOz0*Gn=4p&o> z{KGT7)~YKxNTh63jmvz^`a|WAP>6!hxsZA{>q9>sOSqGS$}raR=wglK4hq!6yV<8J zY5(oG%?eu{`*F)Ft1vXS{AR1MLK)SYb+(K-s&yb}sTM@cHfN5>7E~l%xvo$+BqI>{ zWhsNOhUcK`MHeIH(F(`6t>vt8-j2@Q2${8yF-uIj(miI4#$}Fs6WLr7%okM>R6dg? zpqY-@w<-><%p&?(DK`Gjw41eoBxco_WfZXkc}*zfBnOM)VUN6!(r_d4NPq!lD99*D z<~9(oujUIkU3Td`ywmfYGoOGe4(fhlm7_?P=MeB= z7HPWLi_xtOc(IkLVGzp&{Gh_x9Il&0)$n5CAsaOaZ!FW`nehvKG`{*6zb`?c=yo0NA-V>7 z<#GTr@aR%Pf#uDIO<8~pLZhu#R#xSjfyKWin}kmkPQ4frzq;eMaC>I}-Nsk6Jv1Y; zwFU4nAv&6f!o$4A(jefPi$?G(HKK-7_02UX*XgW92#@n4EQID9WIvyO(a~>*dq;JxA&Q7z(smUB> zXl(-2eR(m<6=c}n@S*W2mm$p9In-Vsu9*-t?m)_y6)=d~ezcqSgIQEjgxjk1XU|}e_Xfbu=P#MA#XI@E{J#87p7)gG0fRdY-JXnmmitthq(n|nq&IEG zY9Q*V5H91b6CyHIo$=Ix)RuB73IUpG<=!xi#IG0$5w{^|iwQab&-}YMh2f9491Hlfb?5{AoD6Rc~sMAoAOO6%c3nMUwAfRz(mqf{nyx z*%al=Pe4)5LA}J5> z@-?Fb&Be}YE_T*vQu#K~zhmjfDawd2l1Memh)~j^XPDz>!DVhHAS#|a8FMg9(7R7T#KZ`YH@RHL;nIC_iussi4<%=% zdVCnTgA96Y4*f9~3q++v8c;0gnA9L&Y5u5%O3sqz6qXWMqUr<3kON$n&RI}j0IS8U zHzCk>Nu~Q)4tgg%(bPi9={63OUvDRqS0#Nh?Q1?uFpHRPRenwCE37q#A7k6ptq7LH zKLK8MDEff5v{ZMQp=8>QofebT&YjhA?qK6XG*uQ+CNU-5XQy#FcIQ(ImGUOhaVOCQ zGllzXY6{a8h|XY~E$YlKQd7d~mS=0Oq~}DLZFR0C2#-&9X3(j|X}a@i-Gn=fB}O1< z^MohT!R#m2xrGj0d4YTJ-cCFX9^2bca&~XyLy#7-dmE>evI|bp4@uO_-Qe(U&IZRf zwvR4)+M?)26JH;nHszS`YCOan1Lt-^Z+}NePgt14X+O?O{-m`@9a=yAz+5Q6B1pKB z$E&z`9P~zjZ0uNpqs@9OCWRKCCn`_M6@XLjv?i{suAt0u8YUsD(v7t}G-333a6(o? zk+xM~#K*Kx5|~&G&{C6wr+)IVFGt}a!T%nU0`uqGU;Bv|FBY< zk^m<#XN62@y^Vu|jbAmzr6sU1@v?07_=nTP$)>7wI-I)X8*}#+RS}kJa*h#Es4-y6 z)}nC{VA$PHyHW>m=Sn%MKB9pdkWug zMTVu&m~lL*%^9uI`)3{X*tG(F>2PY`*%8uJ-0|S%j%O98A|CpaMkjZ4j~+ihHW{EH zubrQB79QV(4i#ll)rI=KY@_$IM)Y@VaXIDFxS}a4=%F}F{B`5aPUbTBRhDx@{3-)J zX3UHfHbN}E&EKKVp6N298m}%5m2n@cEl#$1pS+%}sW?OB*HsEU(65~FhdS@crFf_f zPHdji5-%x;Pr6AJ2XfkpUq4XuAviLHazWU9xwG+V-i*+@JR7hqL*9LeJz0qoEjt(f z$VK<3o(NBId2S8Rl>*o8hp@vg&*70UYjH$fj<*l*T*Z6oC4cu-8u&STBhHu^50QUi zGY#CQd^k}baK)&Y+W6e zg34?9V#*DWorrE1(@2)vS9~@#?;XdB`>ZRHB8C&K<^)P(i!nl}-Ng^nJ!O!O<~VJt zoI_^wQj|P?==G6=DStz4?;aj)?Cdm5%eEx+KoMX-5_`T2xBj@htXxSLqz!Nx<>v2oDu>}j31MSKxB{5eZbbT z^@Qq?TobaGBWy5X(woCL?_(t;?sQRRm)32jM2kwV?gjRJ>yvOf{ z@kmUx!V8`%Ro9_zB=4)!c~Oa_qf3k@!fZS$H7-L#l;)s}#mc79rY~ZBjB=8_Sl4l4 z)3vn+x@7V2GGzX-1@7NG1$F4W3~1vkcZ6n38J|nrncvKFv3>=zwjY%F!%wjc#&gD7 z%2-tAG$DEgo`?gBQpT(ptCU~5#bRcPN@y4dS^IG^sRuDD8-aaHQ;$Kux{i3E3%6h$ zj|_9%0#e@J;#~0#T3C3#uGquUFk<*|=E)BfgDkoPw4&~?p}5_9#I{duQQY#L*Q$7x zd*JxQPH@KZLZ|5)&OS^zfD+(V;n*!6YD|%ZR8c6kO>zka+EQm=;Xnk|Wo#l62y}xx z1Nl_1k1wYEFtZm^<-DLj`}(16H&SReZv2rq_7I9vY9HkEXiu0vIN&t6eBp;t`MJ23 z&CM&0n`aO*>MlcyWX6?r!I_azIe7Z$h^+IWdzH%~Y=3gg$%w7o^`J(n#FWnnFO*nu zwIJOLp(ya1pd!$LZFoDPz;N8cIG*R}0TEa_jUWGctOXcpZUZxB7V6Oev_f0VT1;0k^X~6kS!rtOK4JAf)18)SMH#N(fD+RYvm1k#C8 z1Ft*8^7s(*LncpZRZVpg=WoV78++#$(eRc}SD0iw&)Rttr|g+of2VBG+*zML=VC73 zsaR)UjC_ZEeItKM34;pth8BpP7ZhmC^UZ*~41Dc6q*KDM#7PY=8(z45P#Z%v+%VmW zT$i~3BFQ1(fWXCi!aL#77DJMC6UhRgI*16DguMRB&$E$!%RE5IX4Q&t4i|3Wy2F-E zDoY#;>2{v(3ruu7|?64HO(})MyNXAcP zb|(hY!2raw2Qede=CQx*w-q;gyU@p$POI=ZACw zWG-+$k}1<*h<3Dmz?0Xsd|aAWy*E*#8b|Ip3d^tkk*gR67Qc^;G`NW8(-_qV_fiF8 z0hES@b|7)}A$?z8T$bz##n2_54C$c1Neq2$jkEe=jSpWtU0UHH{TpQL&~I|v0pKWI z-m=VPXZ5x-AI20gw8%`+C3{72E7|{ftIUUh_!EOApUq!#*}z>~XCcj9HF4#UF*6-% zKc9dpz;ryIhL`Sxh&o425oF>~3TGJQxF1Q_lvfGU%qt&%o2`WZaDT5^TBs7YjJ(5r z<8V=)8}5xOzB#bA{9E6O=o^s&6g4n}FuQL=`vQ4Flrtl`{0PtM@l?beMCkEOG>>11 zqJS}*&Qw4*vp`}yitZv`8mW{eiF{fGF7g@TL2g5zKm=x>%Z=krDTgQrubVUXy**Mk zPnb^YoKlEa{a4(PP>uN8d#*7{^rJHHBU2{zns}1vkRNCW$>(4SPrYfl#Li@+JAoPapYK@Js4Vedl)&+A@y*L?T3(DR?x^a$ z1AdVLUA)Ld!(uU+GzX2y_?ziTlAynv%6XP}+~c=BQrZq&ttF}?UM{h3zx`HQRNK{( zaBld<7uaV@$Cz~L^yKTMR9+8ZY)N+|Y&G-xN7eqFbNRVIpyWS>rY1%S*OxOOf8mM(#}}>L$Jnz9v^c^mk^){PzFxoz*JL9Y@szUy{H5xoy1=i@Lz>eUrtOdd zGh^F!3Y}Xr8^7|5wN#`(x6{F3@Efs&+>7aLUhW+{^MY3o1X<2bSISd-jU8Q(qXJL$ zFv2?r`>x1y3Bo;DT3>B$^0`yeciR4R=!}=5baUv-PWKpjJdNbZ89;Y(CN5N^NBowm zfbm3M2HQ`22ti-zCR2Vwsr#K3@BJCEI6L&M?$O5f&S5qS(;DRnbro+5Va(x|`1*r^ zXL}YwCFdmIbMgyb7MpF(3enj?;#J+K8VucpVDqk0dpXmAcSy!I0WNT;EuNeWLHO6H z+A7`1=A`e&fc_A;>kE+OMAm|k?d<&YhV^eF7lAa=ZDu-niC12LNQ+_6plXal0cg^# zGH^e*3>lu>8tk=yjaL(|W&w_`qX59@rVxMV(5<-^Y#gIw)bxp2ZICbrQ!Vz(kYD-v z(BKVrAum_w4da6NBKnN#k;DgC_Kv8YjFy{6w=bly-8>n!EV*Eb&=}`_o_9fzKubxGmXHI zy9}oEhq}?3n@%(f()`W9jcD*bbaUa75w`OILm&EZtz15b@lE>%v|PS)$746T2wX3E zU-rWrL;*cMoGD9xc4a1#Se_g#&n*A1fGF1x2R(F4A?^n#<+B@1fc-1{x$ApNn(3-HhEEB~4heBXk zqaPs8EaP9$e@Mw%nU!af02ge5`-PCOnR==w{z)-d{B{@X6!?nU*09&?8C0r8;>G-q zL+etuH=NQlG4+9(n? z!UILhHXq%R{Jd1lJD;Nf;pe(eq+#aRR2(s=9me-=GgdRIEXnMg_AutQ#>|-^9Tn^R zjGH=y7Ic2>prdQFt2=abVb&!<&93l^GE(oeH=X?+na&?kAHL{VUJ zg5e3SE_oHAu8vX84$EtkbH<2R5zNWnA^XK(bD1pdgVeYH>w_>dW}BWnyobcDMpc@vX% zewdZsNHg?J0G6{~lN|VJ{PZk4!}PR(>0$5I8c9#c$7rqe9lBsNJHBPDHi2*cw!6xI5}kl zMn|MH`_eB*=7TK5|BZ`m@a0n`Ms;A)k`A<#ZXrOhk;wg>m)o*gPee zuR&aJ{=7eV5@lQYvnYZ#R44%fUB`IAP|N4P{>5ft4 z?unf%rn7J4omD;L+jX4g>AlXlvg;ryNQ-<#s%n zD=4TXOy^06kae$|L9p~Bh||b$n2Q^t><^}4K7l*&R-VS4X2k8( zV5nOucA}9tIfTHRaZ4$&QK`73<+Hx5vtJh177|mPPX_pYlF|j{!vK zVMyhO8;%r|!{D*K{q8PcMK*dX<%A%!7;`6&{b?jeVBj>aGTQ~JNa1KGGX785pzB1F zD_ozeJTmEXYx@AZN`9^Bi#gz6rF+r$kqaXxlN$^hsWS@m$=ahsAqFAdMDPG_hc126 z>TG=lRFvEIHjRRcMGM%JgtVk6f|PW}phycyHv=jlEz%7l-7PsZ(n$Bv-8I0(`3Cjg z`}?o;t+Uq4P4=MKisacRgaw5 z;@sbHjN}?nF2XAG1SI+A`dI2@Gi!=480MWpZM zyvOM1Z#{qi8KwAW7ya4V{MG2s@Te=&W9e=d5Ka14f2teK4c4Biw@o}fimr#Phy3(m z;yp5YM`@v)QW=D9W&6HBTWdq#qJ4?BpE0lYx%chLC59K3uTQ@)jI_K@iT2rCm6qQp zTCQ-%F@(#R`^YG)6|BlRGkkyJqIieEegbM=a(m=ls@Tev|P9|ult3> z-5-V3ycN4+wsbo($h9A~IkrEp(!)EtX!9=mzJHNI>d~&hP#xrrKke~Dvkv9eTZ{Io z>2jNH3?f4ikrC>Iv?{ap>X1bibN-TKs)@VVNY?^i;6F&1>3VdhB*8JzENK7sm+6{j zT^vP~Ah#9sdy$cJyUnCFh71bsWxYvy3|-Qp4_~^~e!)$8p>-3!@ zH<@bw-ik0x%Qpj41-*&|omZ50x-Bm#Ps=|MB{rBlt3D4ho4c|;W1>#iW3?Y7@?AA&NBa_(l|sWPzpCDReod!#KI3P)iV#V^ zp{v|Q|K21Y8@DqyHwxu#-6VBMzSSR`jW53okI~roU2|&-34iN;OXLR6wFPRe8;^)Nu3_RDCQUErdi*ZEfBJe) zSyBx#MI0boTt0L6j zngsf3s^HaMqNWl2nCkE<`dWqv(Ow+2oM7N0xxjUuoB2!{SLfBtP%@cgR4;WkKKq4I_8k#&4OM2Gpfnurn7_dM+ckjZ$r}+lzTw5+WgmAlUK?p*i zIkfPy$G7W!7EA5n`~BS#qp!X_enBFqjQqZ~8Xa^^GDq|_ht!EOMR!knE}PZ%&cO+^ zpwQAJcS)kTsQCpC$@&@6Xs{-V!?;2T)E_akW z>M@nQ{-D(_Sy4U@=;k?!memmQJ4Z9L%B088nQ9K&72S~VfRKKxw@>zO>3u925P@8V zhN*m!501DP>cjY%I-Q#4*+Z|Drpll<1UgAsA6Hp$ zz~lvcVeDoa-@gM)NJzU2YaGpSKtWXa9TMh^1WUWV9{AXRn1h~)x{H6dTG)LiwQd5@ z+eU#M@dB(#+v)2jR(1K{ma@a z<;fko?YuF;PzBdc%UK(oM>H}O21$;Ud#_9qeSTy)3k#OZy$t~M^S^o?%l7dG96gpo`fIYoK# z4mL=ol)a$HNc2aZUBB`E>qW9LaSF!L;2w1rvA#{8@1l3xl}{R-^)b7znU5ri=u@<_ zi2160lSLuA^RU+M-x;0}2g;5+T#XfVbuSnukF9)WXWk+yo#)3bZTuv52H!Lc>QA9W z9K$ekA)ui3<$F#CD*ar;YrI+p;y|*HI`K+ImtFqth?lo-;&45GO8f@?NN*P1*M4R; zKC2;48y>#2u)y2?G5Ck0t80le-wZ9IF*Tg6$?0UD1eP4sm6ss5+G|X9q5bS3v3}^K z3<#IqaEsXR@H!P3QMWj(tk=$j1rN6gSa}tyoQ?_jI9`01;JR??rk;@u2#QuOqDc*c z@pf9?;%MQ0a`5Pg+HUa*rR-6usMqv)vA%6}t%4m=owezY$2mO}E2HjJnIcq-d{C{V zqbKask0N{+L>B(BUUO+SD#P&RGcU>dF`>->NNnxqyjg;}5^81!WGA^>M#>RXRmvoUG04bbaS$+O$lMu$hX-sh4#TyvbK( zuT4?X+WY0+9ddsH*1K0xcy;|e{W6SkU)%`zU`n2D>2$o z9f{W+ZWXxNTH+H|qR7oWl;EwHL+}K9JF%$rd*M68bR!(^Zq>R57L>ZDGiRDb9=O-P zeW<{nQ*@o#m64bEGoy`3cZ3=*Te*DL1c`>;4MLy4Q@$HGE6xvo{ z0`iwT0VwA5S$Y0gC|cMY3SWvqAM4cBAW9BloPvj~ZF3vQ0dRft3_5q9nrD(xkdUgS zJFij_;x%7;8dDg9ML@nytyM~N6XaeGVXjH!9|*GsNFO;G|fd*9aYie2J- zL~G@>*;%}FQ8HVw!5Y8>H8Hpd=+>{ ze$5{>QWY%XxWskyo!Xmq?s-0I=Dk#At`}CT3R4gub(SpBzT3XaX8*fbnBPdG~NzfLj%5uu5r?`Z6v})e&zTr zRqDYzsat$s9VqT~q1H})8n3J+K$gpNzjSDDuCM#@UH6jh9Ac@-`0VOY=zHTqoKB>l ze=6_$RK_Mgo?C|eL}FJLm)~+|&kyh?z8iF#d;65#^cFis>NU0}Ej$8Rt-`7C7S)#b z7;0WS&Rez~ZhusXVR+U$t@eHII?x~KG)eddNPcd07!RPd6w*V8}n)F-beSX2)7V{*^E@H6U}XNjyC z?$`${i;5TAQuagcuT&jA`x#V5NX5#5L20VSv}7+8y+1;vv*l35ckhTdtgfH>csUjHB<@GV24KjO>hAp70^&}oOQTRO5@ zt}2B~lSAgB&ezCd;SMXz{(H8h-F3e{oy2*4-MCZL%B9J_(j+XL;t3uj`j#q6>50dl z!1OBH)0~d2!<`nQUX!7-xi6q>-UjKBEF`*-F%%I$&UhlyU8TQ6-IvV?n?BueI9WLM zbSJ$nT@6`+l;_T9CVYN6o~>Z;hOGA?Zp2hrEOo4_%kn&(IUZ!Adcnt@zHyo1QdAb)8`hZg_LIk49qMeh zaw0u?Eo^HtG1}+p!RNXx?1tX#DSd9~Ly(nN*P_OJ&xktENfXRaovmm~yE5giEaqS9 z;NpX3^gbNDLR7M1^#O$(y}86uJTmyOK73c?Iny?|Tiw%1)nQbK>C&2MHioU!sYLp+ zDY@Xa_r$j#x{AKKzg{~KI{6f2J5)uLv;2vhJ)#jXP<9& zP~d%1_S||0V~V_B>^a7qz*bF6{NP22IwRjJaSEY=|}%4SzSn3N?)6r~>)@p-)$A zRwCUt(Xdqvn``gX;sAQ+A1ARSW(>qyfJ&Ly z0kl*dNo#Qv1xo>hXpU9v-+VZPF3vTMQ+@|Wu3g=mK)vn_#TlG(01VO6pGDc zd!@a8ddls<82M6cY|LXAeT77JbV&6zef>#GQxSm@6>HG^v3}C6C!oxqde7`bzi8xz zEai)JQ?6ldH$*zpwH>nBgw*>3Bk?KPamNR7c>N1?)-(lY`hiU~0hA4mW_}{y84(k! zyL)j{D26BQP40I;XWiEM?3mXb%C|eyDo$`=2UBt;&I%CXtrGn?yq+~WG>=ww zr|z?VqbM8A^=k~sPNi_=3c9X2({d4+v zF$2Db?Qzoe1`y2!yHV+B=+jh!83Qkup8PObKQm{sTfzgWx{E{`*yq`=%oj!ZhdS9i zQjD(A|Tb#`IMReg_J3VY+FJ}U?t-`JrUbsJ{J;cnmE$SCoNOG0^TMMe={ooKE3FvZwT19`Y% z6R2Xh!#Y=uf*{ZC@olKT(fp0G8{&TTclP*NlfpYZ28KKjTRJ=LghV*88vdxiwD4*x zf1FLrv;G--eXa4L6vbsb@rpZzbvL+!#lJRQU^0GSeH?jFfjzC~yLqBeqe9y$!G+r* zU3(nIyxbb<%eO_u5ad)0;jgHvS#B`VXn*DAaL|~1GbQU1w#+nRF2S}H_G2UUK5Bdp z_em+~lf{n_k44{~dK(L0w7a?VLUVy19&t#@l)b@3tN2UvB@^?^sN%NEndJoXi_916 zW0*e*vI=}Kzc0zFpegmBYYhym=SB_#TH{h|e?yitSi=Ru(_x=H?+Mn^=HR@mbRkG@ z-U<#0TvgpB0hc@JmtL7|L`V=F4&Z!}^6gF-J&?LL)U{ecxlb=;?P4A@qmVJ@L)&=; z*2Xo~%gW&@vZZoyAT2dvSGP?-srzF465%_x-ZnqxoQ&szzV>C6<&|wI4BbtfCG1M8 zpQ5ixPO@(IOrbeip1;F;QmpYa-h|n(x3x?`qqmx!1Y(1Csg{kgVUg9D8NX1xa{Ch{ z#({~;P01j4^}RgnYuS0qL8orzi%0TFu2MkWx8UP|&6OM7j6O2`XS3^1?=0>8yxjYE zmd;*fKn40EydM7jrt9nbJKsMv36xj&E}$J)zu}yc^InxukGV`Z-jMhF`voP{x_SF6 z!5>7s^1iUK&2$iLN88Q|;pqHuPL`z2|0L-9l_Pw_LE1T&9oK6=gmu+iuSUC8rSs!c zIi-FK&tl=}OFI?Z)#_nlo+|Ca3mSwk27%>zRoIVRUpyLySF#k1*A zN;%W+M^N5l)rPoY>mPk>eFf31x!6Rbv$PQD+^Xxve=RKTOC=|eE0kNKw{yJzo+Mg1 zSfTanK9O-U%1YK&VvJ=A&opxU=Md^>Y zhdNdRM$6w%w0}2Z?PH<9MAxn&d$$6DI@GiMf+-d717JhkbP*({urGl0{6c=39Df5g zdkQHRWCa?zN6#?eUf1r zAN|r9T*bTLrXt}W=wIa|`mp2j3tG=`bB=wt6DkEcPGRL;_@8SmbWM5+kiGI|%M zaCshyEN`v(VU%=@#Y)CLS@iCwrIHUif-F3-Ax3JYP)GXu6?xmwTw7I!@oT8dk$Y`* zVS~;^D$afKi|eaK>H=2dt)<^)e)?$HmqpZ35|VG;USUoWXAV`WQ1F~t9a*ij8gDLt zaUvzler2!|+f~40O>*f)l5~)j(t=++)of4d&Hk--#~~GP`%(VbAiGlaac)168iPJh zqrQ-_QUnc5H1@5A3hQ3QDQ+eSli1WF<^^3i)J+v{Y^1!SOziMowc1lw=BKGP`cCMxIGfMXFn!*GZu%VVTi7gykb>=Zgm%Z-6X-oMOF*(98I7{oAm}z%O8qwJq5{aARXr^)SYkdA$#@N zMWWD3*ID@xZbq1j6_0Knrwv}mm1?2s;AZxRvzu1*TXd+L^uEU`i0gsPIr{Eqbd}Zb zb-IK6jNHHY`kC&#MmTu>h^dt=xWb^dgG%AwZ`&>0RM(YExM{)=6HL5vMTaYDm#JSj zzuW-7Y9ByzHXKaK$Tgg*mfpa4Hc7;hc^eevGVm zvMPW-nIAKfGLfem|IsmRO+C(GbbWNIVnaReIAzB4h&Dx8^ULy2 z=uUkMO2f?P0I0GC%aou0+`1lxU}vSxQoaqnXxJI>VP&VhOn~1`Q<u>1*-x)Vmg* z%u25or?=`#3m%Omkq6C)yx{uwEcBw#?+Ed{d#hM8_=xFQnK}oQ8q3imz;Drd+Lzo2 z{Ec27H%BQNj%0dQMhHLSxc=5yc1<}&gTtnsT6jhLm}H8#-d#yjX?5?$Y`LW7;M`<6 zt)we&vu|DT!VJ|f`R|dO5V+tZe`>7weM2u41hG^nj1TNiX<9!DD?Yw+TmzAQg78clUFSEfmb29>m zp9-m5pR=_Gerx#Al&W6Pt<@f=8d7b2?8FyRUChLDOFvk%$5iC)at@UmFznXV)Q_vu zt>H5rON`Rm(TVg{u^vNL>zGxVlMimimun9e#IjY+tFodcb6>{}6EQI#skd5cOwd(s zqeQti9CLVUFYCpoBYk`%itrB`ZqD2K&I-vt?%BNbDvxVRTc--idD1}C<}mxBfNK=- zp!RL`><1q-78k#L2I;9-6o{-T0S2dVf>&{2peITT+r6g=!w<(~>Mtdg;Avl3C4A1> z)F3@~%mMA}Hu6v9gpr@Hl?-1A7H zRw5aMjecQbf7m^7ojU%Sz>1uH(lsUK&TzOvIO0@YwsX+#+f<0}xT|CB-l4HiPPXhv z%H}50X__ZJ)#{x-?p-7Wb$cY!CTqv+5}TguRQ%KTUygg87=-r~^G-6nk-*pgl3hm^ zsPkQyxLFU6_1jq5B&V&EMRw!tGiI~7xAZB{z2?Pd7}iDD?~C7}82bG7so_?`F?*Q31VM(2}j*Ys7bqO zkWJ0TwnQe&al!sHErH$Yy!DUymLp81c*?k3jh2TtSTcQ>pVhI(nip9xX_K!$Z29CU zM9(hXI9=J2$2?=&dXkr^zeFgcEivbx*4D|;7Z?b*aRSYq1<=RXm-ZM)=&2YdDsHJ_ z+OuxyP-j>?S7fy^Pu*lTZcE*R^F8rVz;KTJ`p2lr0vxh(%L_ zhMl;JW8f!fodv^FXzpi#&fNoTnFm0!8(5$vVZ&iv zx2^>4LB|51oS?AM_yji2bOIm&fIla>6GJ;3gN$dZoN=0}n7lQXs9iJuu6*69)Qq2| z$9vKgt5s3cvEGJv>f*PuMp@L`x60zI>c$^?@2Qh)|Lq%&oiQ^8j*>$HaSX(wX9~QD zBLrY$7c4XGVT`*GtoSt-&xD!4tI2W;y0Sx!e96Ha?Fusui(vB_i{eQA;Frn?0k#HEV=mZ9`2t8%pgEoCoxPg~t{^(-ar23@!$gQp| z)t=sloO0r*;WCTJia~}kx-KFgI3MlDj6r&y$)k$P2`TVmq2Yk>g4muWQ21|c;VTOC zVIL*m1W;a#wWlCZ=mnFPYsRzrbsD-YEK4nUS+=y{^a4E@OCuAB-g;(3XTdUqYYhxt z+5E)i>ZLPg@fBM6m(I}t_PFK{g?s_v;vUoo07wWJ6-xw+dr*h20)a^Lt~|93NBp1e z_!Msa=DbsF4Mb?zh^@t|NiJ6Ot3k60OOx4ZwDw?G{72(y^O66>**PH4)W_Iat?~LY~Pn(qs($!4Z=ThD7cZ{Q(^lm5bDn`D7a+<~* zHQh-0pS8oLQCRfeJB4XwWBZ(AB~SJ!EQ76<8~tVrt{Os-0TywWXHf-4RplXSmAS#% zrFt708%M4++Ql^Ik9koj-tWpA=aa1%#uzHxg_iciW&p@X^nB-@|9>{uMk zAGDr8^G~TO+S=QIM&JQ}QGh~^k~4^G88mlr2gfISgeKUrm5>mol3&R-O2@^OqkZc5@?oOPjc z?1tn9%hV}b#(m`D{H0zuv8}4ZR->2CV-d9!mtVc4nJAvGY?I?LnxlD?V<}XjHw9Gw zS{k;S69#^YC7NphU>Bc=U3_8;AUo^1!in&l%n`6ko8XkM)U&RR65;I8u2bLQ&l!#y zwyKzlyG$sbIc{1`6LYnn*MhC~<3Ib<2mUF7`0x6$lz7U00xiW7EgSZwJq9j)0?nMx9=lu# z-?k?*ZZ|u$B(=wQrqAr?1i$KfyiqVspuXWKs5Y8k9%o!U@|g2kRwv7Q&sEQOn*UV zw_15jc??{No$JF;s?=6p_aby(>@*qG_v0?GwgF-Xz}*}rhV79lHVxY6)n^`^@)Phc z5{;BMo9D($l|QLlh(R8-Yef3wmOfugFSaS30?8-^3?tw!Xooh@E`lx;ESU-*)ZViL zM5+^NB_=*Rt$nwV^PLUD<#cn4Ig#4fT^i(_cU(dXQydI;y#;C_>%W2ya6XR(LDZDIMmX=7k`>;qlSS%0rZzH0NH!6Bo3TR zG+3V-@?)4hqj_dw#j|X{sw(rN?GknFVNS(a!W$$%b*rQm_n_gGyO(cc_fP?2q8kkC z@i8lBKmq$g6s*mhfod^r-)`LPoD7eAWyY&=S9MFCn%pSND^p!Au4g}cG~;gnh;j>@ zZ(%rg8-o1y81}AJw>@70cMgE3HcCpE5`1<#WE_^4W?@q_xfa}75@WM&TSkwht!%iM z@k74BB$usio~d+O_8TMmTm)95yr&A4KLLIRPzVG=y%actOCbA(h26dyA7lAO9{Kl4 zrl}$hPIGcWANoOzxTcL7tM@jcZ&2jIKbnxo!0!Tu0sw|i;C(BA87RV-h1@>{W_wTw;mSUg z!L^fsXJr#PNN7W!%&(eYL+iyhNy5oyx^Av6J2}fwn>K>mTi(p%bKI`{ub434?1F^@ zOd$z4ivk5!9M+w%dj9wAIX&;4eYS73n2B+k5PW%^>k*3Z_9k9G^cpa}#@Vs=pFwW0 z?SrK=m_iUZ+C)K-{exG$9u_#<%=_q5YkIS>;p!N}*)Idh(coS*iT3h0kB`t`Q$whL z+Ex`fH|g`0goDa4(1+cJpmR7VAc4gy9=<+49ces*%1E1aNQVvy&2`aYC1SrFdo>N) zi=4Jb*WUPn*B@4}d4V}#TgKu7Ytg5uQ>bVUKvXdaMySWLC%$~hr@dl(AHhM3?pbM6 zkKgZ-eEO}K(mvOVJNN!?C}D9xep(pq6g&kGi>HMv*sK=UOV9Kr@3!D74s&IO8k50R zBldpy42t%JVl&vDHJ47AaaNN$@9(*vGs;h(vk0<(&wk7Od>>V+k@tsg2fv6vHu@M& z(4H4k%vsjF{P2=Qj_F5Ic6#<-*+Op#Q~sU&2HOZ&LIK7$s0b9DoHPQYmqcZFclj08K+;DJsp`p_-NwBWimi1CJ-b!SQ-2h)*^^Y}UO@VbE5FtS+2F}RKSeG~S*+cCkd=EW|LS(PC2Pa=tkP$Y5&!-Wvk(6YxC;tlfMWx= zyQ0LP0FM3tkKevVk*Gz7@WyR7iun9e+KD!gpRfPiIX1hZhE(@Xk@iM*eN*~lhFC%k zK&^y!LEpG011*3sMuE9x4!DsRkm_cYi zK6HEc&_JnY#Kzoq#zTcW0dBt7*R1xJ@|@{UwCB z?zjvlGrd-3>Hd|U1(IQ);^ z^t(STU3x{@zNwmmMKZzS4FbFjCR%m_dlKas?mo;P@wLWRxwl}MhISQLY`DVNdFW@xo9>`

UQUd@&ZD{x5k~HJS&x)PUpw%lY!3x`(k^z{7zd#> z$h6pYL{iocM6}Yt6!BPsShIij@$#FmC5OTB+hji<27b`S;aGTK`TB+nt4JfnHtbB7 zVg4?1$y)b&h2@>rXNnuwjoFcMU17>vk`nL=@6_<>(W^{^v8*Si`Uuh?q;S>CzuXE$ z!JN_=_;{Lp3JnKjOJWHfV6db`Zd@vvlb8yIO*8y)xzZCQaRf7!02^%F|yc6{O()echmr7#0;h#0^$R)QS1*GI-!E$Ak)Y{U57Zl z#CO=E$v)_8#n;qt7tOmKmhk?9G}B~+B|*66aoS*qpVXh&?SytOnIRou-@XbXb_x_R z2eGIXAxvPuufky4NLyYSmtet+>~6z6=fmr4xT-|#iZz>UN54!B9go@nMqbv<(35?n z)EW)68#o;Riu@I0;R;+4p2i)U%^PlNyG5j#{bd0I@R06q{C?Dy$@jt# zYS)~3zL>tWW2k^Z-w^$=h;RUHfHq*1PGMq6>u#qY{o!vNgL>6IUN4W;Y`I@!m?{|~ zQVu2l15)|C8^dMs4&wUUL^uk8mW0C3e{gY3G+Z8H*}a4=y;E^x%A=8*l2qmF^K zM%HYa=FnrJn4I$}&CjpCb7~%w>hY?USH3!b&@$i}0RN1c1F`CkGgvo>#~=(q+QBIJ zRd)ncl4SJB){Z5JFMo#o`YuqDwY7omQy_7l+5`z-WOlF#Jm96^$V<@m63Ajl*9q+j zgbnL${mvLNK{^nb0_)>KPZ6n^ELX#)ag9IHe=qUn>`u*RC$xpLYYNWaeK3%|JeX!C z7rC9=m$U)?&W8&_fg#3X~PRf z@7YPXQoaLwFzR7j5X43ikWW#RuNX3AU4F@CWE<}mMZr=Qp2kIzVog_rfA&oUAy%7r zzW+>U4)l&NXzoJos9vT@OtHQw;rMuo*WMm?eg7fbB{tQ~JH>^|+CLmJc>^TUpJc|l z_Tn3V$EqbA7zyhEx2@d^dJCaDpb3e9+F`J=QqQr(J#5Y3(urwbw)kmU+Bfd)mX}8H zJy(@-Gd)eAd2FTX!WZV7mVt9zZ)}GmN20?p(dT2Ya#zg#Q^B4A=sJZR!u*g7Nn6lN zEwO{f7dS=iD*a7*&r{o!^SwD&=6r5fEA~93b`X?q!`6K})i-*Scr*iqfh0g}B2u{A_&L+PR!IkfxEk zp0n90j;$vneyi}kGky%^+gYvd)Uov{pW~^$g;2{{b`?<_cz1)D;6GJNbU7M@}H#+B4F|Xv?qTcJ@l`o zqO5fWv(iU-ziF8brVDL5ZkAfHry5oa+h5+wuv@G|mDCGuzHahQL1=@4DmBvlBy zv;Z4==NifylCyFK7d`9+XZy^p-8gX?A#552ziI{q1DYDWz66kK5{!5*t-qji%C(R1 zrT-tJ#|O={-M|PPdI;mj?otw%^qV=)Qa;4y_}K*q7|^!-L9It-w9`=WQM;dP}`ef9S|3m1?Y zu$My`3*~iF#wT4)9J4Q=Hs6EqOqe+IO>Jf}Ku&X`H+J&h|KaabAcZQd)C6khQ4(<< z7&=&iuGgI=Yh{~hmOkX`;+lroEPb=gUpJ?lI$IYRVR@*o{rShIARnL3&ePVa z|JW$+;FqeKvp*0|yeiZXPe{wQEN2U71L%rzDa4~pWHTzHf7gNVxIv&K#Vk2qtS zt@dQ?L-P&2d`qFXR~$D8-S5c$xAWmgFpD8*{!St2`)yZe@Tw!oUjA8OeP0nd_rn*~ zrC*pYl(01`MU_Nr8+HhVyWI2%#CB5asUe* zB35FDGvz98!i+}j=hnuNFL&xR^CuCK_j9}>;xOQw~v3i(T|&ifb*Si)Xk=mr!S z7w&{6BNzk)*LM6na(5P%$i&=hMAC%Yleb+u`osVVHOdIj>L4rhblut;o-71C$R-mT zEepj$JBnZ5>}YTFQ`z5CZyjmt&u7%u3Me`pSLeue7>6_+_v~~-P-mr3n+E6@8AscO z2H@7O|ACkq{5nzyJKakvs$bJV%*>%T#XM>H(8EqJf%Uz<1N1BwuS=q|;D8!(p&9;;U5DZ>wK-`O}-c;1BCcO)Tn+ zgj3c+N4K@uXv&p%&vJT%`3sj(3jg?B4pZ+YhH;<>uK}4=7{IazM3MVTr5u<+7-oaA z4+Ap+NT^sK6kvuo7?RN&FbstGQ}Tza+r3T&*SDYVd@rI_OuqK4?(rzOa2R1+jT9^4 zbU;A<(&Wv$z3oJD*1l=Bb;qBv)Ru!-xC{U;Q7O=@a{`(9?bw6_;o}%TMlfk&IOT6X z-qtd5&Gpj$=ZQ1XIo@M9epTK)+f}9tonopgi$})YiFtpWb8;GRIQxOo$f@0dY5_D$ zSb&pT>xO;ByhF8$@Z^q;CcNNn4dO?{vwRu;?6m!?$+G}Ww#jg>yxtkv2sA$qK|)$S znU&7ZzV^Q++cyS^Jf_YR7>;36AQppgIe~9O{&ENv?I!=gZ~$TXowA}H$#;6k@%5M9 zjwz+h2;z{irtvMcS-g)?{Hx9LSO%_xahgi1s|QOMERFOv14ke@3XZV^$?vvtKj>2~ z8IFZ|Ph6;f!6dHKG(6(H?o;)N3W?E114Hlr+ECA^~hx=!a zV8>wyuzZGPxxR5QF|Ol&&ij?FeizsY%FqQnm8!lyB?)>`Pbz*L3!Y_Jwr;AjcDBrm z|61W``b!@Ftm{Z;3iO=YeqReXJ4HZ|YZ#9B5gEIlG@96#gQ66pqK>5M)cHr~mYhlO zWyGLNU-k$M&uhzf{A0K0E0G0mUEqRsG2ll)zXK0?jFj^+2JW%i0e;rn8;UMt`kx!q zseev+^}TT@gxL7#t^@M59bI@e8Tkv}hq2%RHqQ7ncJ#mo|E}0MPa@?|g~>ZmbQv(L zsQ0@SF^gdj+w_upX5!QL+H(!^1r4I@F1MTzc z&Kabp6$3wqCRRBU<4EXl?>b`d|IjGvgF{TnD=98hNYQb8XkFPIVJgDPE0W0;`&8w9 zT;+z_;6L@awSyR||CCdS9Ie7slGPoEEoxVeuthQdz^VOAo-m0bn*8E#YB^_I^<5JEr56-K?d zf*wjXwq46$jZ+wHOY^s0qCL*I@VSrkUV0w*&nyrHpb;D1Z@|)l2Rnv%1<5dV3TNt7 z;$=~bnL0kDc{%qi&GBK7$5%n=jY}~+PNfwbS0KYKzAO^|*GT^-hTVse?3^*+8DU@> zDD(o{^K99n_GAJL86dRY{s22JxAWK?ZB}sm;2qjS#}gUkoS(v1N&4ZyRy%hXT_4R} zEfN-GV{$7$I?Z=C;K9a6#3wOl45ZyjY%|=nuXsiX0Z($V8-RHn3=0)PF$*@qU%a~n zX2kF=l&0xRYno0*w<}te6_R}-}hI%>AL>=uUhr4!@zkNjiAd!I3gi8*ZX zmZ2BexL9Kp_A%H{(qsi%Q<#|TiTtVZ@F|hSWf`+K&G`gTXsZtaG|vN{<{Nb$iUtT( zOD<&$MvoR(P%S6U{$ae$gXAhO*XDd`+a@Mf0LTXrD6-=&tmnH5rCmXu#WO(FvTM4I zXF7IFz}}$qc40lTH7Nv|v_lL#S&E@N`BwWQ8F$?CY%db#TD48EIxeWp zYegP_-x;Scz~9-0nRc8ePwoO~xme*-|7^E!`)G(h8U_KwI>a+QC!0=1-4nm2vnh82 zYIyTXoWCd&Y>GX5q$|3C?chOUu~ihhPL9w4-qgvk+}*c-BYl(__Rg2$1K4926dTh* ze`9JAy`hUg<(IS%Lb6PYhlYG5h6kozlUT0>zNzD|Aan*sXI2K?9TPRpdn7@P_vkqP zV*obL9GZm96EV?9vB7hEHp8&r>VamrQ@Bi^pxLcckxMvn<)&VEfye~QO4GAAZC_1J zm5BGpR64kNj zs68#}ShaJe-1O3?th`F6Z&{h~S%)L7%(u9L_B!2@50vK#<3FQr3%a#%2D#=e28d

?{!SmN`Jv=Jq=lu9 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 literal 5343 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 diff --git a/testing/make-archives b/testing/make-archives index ae7d58462..b2b288cfa 100755 --- a/testing/make-archives +++ b/testing/make-archives @@ -35,17 +35,20 @@ def make_archive(name: str, repo: str, ref: str, destdir: str) -> str: """ output_path = os.path.join(destdir, f'{name}.tar.gz') with tempfile.TemporaryDirectory() as tmpdir: + # this ensures that the root directory has umask permissions + gitdir = os.path.join(tmpdir, 'root') + # Clone the repository to the temporary directory - subprocess.check_call(('git', 'clone', repo, tmpdir)) - subprocess.check_call(('git', '-C', tmpdir, 'checkout', ref)) + subprocess.check_call(('git', 'clone', repo, gitdir)) + subprocess.check_call(('git', '-C', gitdir, 'checkout', ref)) # We don't want the '.git' directory # It adds a bunch of size to the archive and we don't use it at # runtime - shutil.rmtree(os.path.join(tmpdir, '.git')) + shutil.rmtree(os.path.join(gitdir, '.git')) with tarfile.open(output_path, 'w|gz') as tf: - tf.add(tmpdir, name) + tf.add(gitdir, name) return output_path diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 6c0c9e5e0..0c6cfede4 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -1,4 +1,5 @@ import os.path +import tarfile from unittest import mock import pytest @@ -8,6 +9,7 @@ from pre_commit.languages import ruby from pre_commit.prefix import Prefix from pre_commit.util import cmd_output +from pre_commit.util import resource_bytesio from testing.util import xfailif_windows @@ -72,3 +74,14 @@ def test_install_ruby_with_version(fake_gem_prefix): # Should be able to activate and use rbenv install with ruby.in_env(fake_gem_prefix, '2.7.2'): cmd_output('rbenv', 'install', '--help') + + +@pytest.mark.parametrize( + 'filename', + ('rbenv.tar.gz', 'ruby-build.tar.gz', 'ruby-download.tar.gz'), +) +def test_archive_root_stat(filename): + with resource_bytesio(filename) as f: + with tarfile.open(fileobj=f) as tarf: + root, _, _ = filename.partition('.') + assert oct(tarf.getmember(root).mode) == '0o755' From a1b462c94a94aa15af3d676700c834f79d2b2b7e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 6 Apr 2021 08:18:14 -0700 Subject: [PATCH 502/967] v2.12.0 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 16 ++++++++++++++++ setup.cfg | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3193b8cca..0c6d63606 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.11.1 + rev: v2.12.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index 5da78662e..2d6a35d9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +2.12.0 - 2021-04-06 +=================== + +### Features +- Upgrade rbenv. + - #1854 PR by @asottile. + - #1848 issue by @sirosen. + +### Fixes +- Give command length a little more room when running batch files on windows + so underlying commands can expand further. + - #1864 PR by @asottile. + - pre-commit/mirrors-prettier#7 issue by @DeltaXWizard. +- Fix permissions of root folder in ruby archives. + - #1868 PR by @asottile. + 2.11.1 - 2021-03-09 =================== diff --git a/setup.cfg b/setup.cfg index ceb1cd4c2..b336e5829 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.11.1 +version = 2.12.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 5deeb82e0e01ecb7cc5b22eaf2f83dc93a92f7d0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 8 Apr 2021 19:22:17 -0700 Subject: [PATCH 503/967] Update azure-pipelines template repositories Committed via https://github.com/asottile/all-repos --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 34ace234e..58dee74a8 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -10,7 +10,7 @@ resources: type: github endpoint: github name: asottile/azure-pipeline-templates - ref: refs/tags/v2.0.0 + ref: refs/tags/v2.1.0 jobs: - template: job--python-tox.yml@asottile From 12a7075fda885a7c241aa137238681f2a9d7211f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 10 Apr 2021 00:37:59 -0700 Subject: [PATCH 504/967] skip installation for SKIP'd hooks --- pre_commit/commands/run.py | 8 +++++--- tests/commands/run_test.py | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 05c3268e3..0fef50d1c 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -271,11 +271,11 @@ def _get_diff() -> bytes: def _run_hooks( config: Dict[str, Any], hooks: Sequence[Hook], + skips: Set[str], args: argparse.Namespace, environ: MutableMapping[str, str], ) -> int: """Actually run the hooks.""" - skips = _get_skips(environ) cols = _compute_cols(hooks) classifier = Classifier.from_config( _all_filenames(args), config['files'], config['exclude'], @@ -403,9 +403,11 @@ def run( ) return 1 - install_hook_envs(hooks, store) + skips = _get_skips(environ) + to_install = [hook for hook in hooks if hook.id not in skips] + install_hook_envs(to_install, store) - return _run_hooks(config, hooks, args, environ) + return _run_hooks(config, hooks, skips, args, environ) # https://github.com/python/mypy/issues/7726 raise AssertionError('unreachable') diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 4cd70fd43..8dcb5796b 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -600,6 +600,29 @@ def test_skip_aliased_hook(cap_out, store, aliased_repo): assert printed.count(msg) == 1 +def test_skip_bypasses_installation(cap_out, store, repo_with_passing_hook): + config = { + 'repo': 'local', + 'hooks': [ + { + 'id': 'skipme', + 'name': 'skipme', + 'entry': 'skipme', + 'language': 'python', + 'additional_dependencies': ['/pre-commit-does-not-exist'], + }, + ], + } + add_config_to_repo(repo_with_passing_hook, config) + + ret, printed = _do_run( + cap_out, store, repo_with_passing_hook, + run_opts(all_files=True), + {'SKIP': 'skipme'}, + ) + assert ret == 0 + + def test_hook_id_not_in_non_verbose_output( cap_out, store, repo_with_passing_hook, ): From 30649e7feebda48dde0740657af6e11f654d5890 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 12 Apr 2021 17:09:59 +0000 Subject: [PATCH 505/967] [pre-commit.ci] pre-commit autoupdate --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0c6d63606..e61e688db 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.11.0 + rev: v2.12.0 hooks: - id: pyupgrade args: [--py36-plus] From 4f2069ee9aef78dfd0ca059c338b8b148429b48e Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Fri, 16 Apr 2021 16:43:54 +0100 Subject: [PATCH 506/967] Include PID in patch filename Fixes #1880. --- pre_commit/staged_files_only.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 617930102..48cc10299 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -47,7 +47,7 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: retcode=None, ) if retcode and diff_stdout_binary.strip(): - patch_filename = f'patch{int(time.time())}' + patch_filename = f'patch{int(time.time())}-{os.getpid()}' patch_filename = os.path.join(patch_dir, patch_filename) logger.warning('Unstaged files detected.') logger.info(f'Stashing unstaged files to {patch_filename}.') From 8fc66027f78b193a7e940b10a3b9320b1641117e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 16 Apr 2021 14:14:17 -0700 Subject: [PATCH 507/967] v2.12.1 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e61e688db..214c2857e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.12.0 + rev: v2.12.1 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d6a35d9b..2f154c447 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +2.12.1 - 2021-04-16 +=================== + +### Fixes +- Fix race condition when stashing files in multiple parallel invocations + - #1881 PR by @adamchainz. + - #1880 issue by @adamchainz. + 2.12.0 - 2021-04-06 =================== diff --git a/setup.cfg b/setup.cfg index b336e5829..400299870 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.12.0 +version = 2.12.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From de2b7b6dcc3b018e3f630b1716e1e5bb177b6f39 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 17:08:45 +0000 Subject: [PATCH 508/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 3.9.0 → 3.9.1](https://github.com/PyCQA/flake8/compare/3.9.0...3.9.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 214c2857e..f893f3e07 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: - id: requirements-txt-fixer - id: double-quote-string-fixer - repo: https://github.com/PyCQA/flake8 - rev: 3.9.0 + rev: 3.9.1 hooks: - id: flake8 additional_dependencies: [flake8-typing-imports==1.10.0] From 60bf370a7da71026ed5a1a4a6d7e2691b8d5bcbe Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 26 Apr 2021 17:18:40 +0000 Subject: [PATCH 509/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.12.0 → v2.13.0](https://github.com/asottile/pyupgrade/compare/v2.12.0...v2.13.0) - [github.com/asottile/reorder_python_imports: v2.4.0 → v2.5.0](https://github.com/asottile/reorder_python_imports/compare/v2.4.0...v2.5.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f893f3e07..6438bf835 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,12 +25,12 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.12.0 + rev: v2.13.0 hooks: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/asottile/reorder_python_imports - rev: v2.4.0 + rev: v2.5.0 hooks: - id: reorder-python-imports args: [--py3-plus] From 6d5d386c9f76c113ba2f2992aa73f56a2a407854 Mon Sep 17 00:00:00 2001 From: Oleg Kainov Date: Wed, 21 Apr 2021 21:00:48 +0300 Subject: [PATCH 510/967] fix: fix path mounting when running in Docker Currently pre-commit mounts the current directory to /src and uses current directory name as mount base. However this does not work when pre-commit is run inside the container on some mounted path already, because mount points are relative to the host, not to the container. Fixes #1387 --- pre_commit/languages/docker.py | 33 +++++++- tests/languages/docker_test.py | 147 ++++++++++++++++++++++++++++++++- 2 files changed, 176 insertions(+), 4 deletions(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 9d30568c5..5b21ec94c 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -1,5 +1,7 @@ import hashlib +import json import os +import socket from typing import Sequence from typing import Tuple @@ -8,6 +10,7 @@ from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure +from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'docker' PRE_COMMIT_LABEL = 'PRE_COMMIT' @@ -15,6 +18,34 @@ healthy = helpers.basic_healthy +def _is_in_docker() -> bool: + try: + with open('/proc/1/cgroup', 'rb') as f: + return b'docker' in f.read() + except FileNotFoundError: + return False + + +def _get_docker_path(path: str) -> str: + if not _is_in_docker(): + return path + hostname = socket.gethostname() + + _, out, _ = cmd_output_b('docker', 'inspect', hostname) + + container, = json.loads(out) + for mount in container['Mounts']: + src_path = mount['Source'] + to_path = mount['Destination'] + if os.path.commonpath((path, to_path)) == to_path: + # So there is something in common, + # and we can proceed remapping it + return path.replace(to_path, src_path) + # we're in Docker, but the path is not mounted, cannot really do anything, + # so fall back to original path + return path + + def md5(s: str) -> str: # pragma: win32 no cover return hashlib.md5(s.encode()).hexdigest() @@ -73,7 +104,7 @@ def docker_cmd() -> Tuple[str, ...]: # pragma: win32 no cover # https://docs.docker.com/engine/reference/commandline/run/#mount-volumes-from-container-volumes-from # The `Z` option tells Docker to label the content with a private # unshared label. Only the current container can use a private volume. - '-v', f'{os.getcwd()}:/src:rw,Z', + '-v', f'{_get_docker_path(os.getcwd())}:/src:rw,Z', '--workdir', '/src', ) diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 3bed4bfa5..01b5e2773 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -1,14 +1,155 @@ +import builtins +import json +import ntpath +import os.path +import posixpath from unittest import mock +import pytest + from pre_commit.languages import docker def test_docker_fallback_user(): def invalid_attribute(): raise AttributeError + with mock.patch.multiple( - 'os', create=True, - getuid=invalid_attribute, - getgid=invalid_attribute, + 'os', create=True, + getuid=invalid_attribute, + getgid=invalid_attribute, ): assert docker.get_docker_user() == () + + +def test_in_docker_no_file(): + with mock.patch.object(builtins, 'open', side_effect=FileNotFoundError): + assert docker._is_in_docker() is False + + +def _mock_open(data): + return mock.patch.object( + builtins, + 'open', + new_callable=mock.mock_open, + read_data=data, + ) + + +def test_in_docker_docker_in_file(): + docker_cgroup_example = b'''\ +12:hugetlb:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +11:blkio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +10:freezer:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +9:cpu,cpuacct:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +8:pids:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +7:rdma:/ +6:net_cls,net_prio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +5:cpuset:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +4:devices:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +3:memory:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +2:perf_event:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +1:name=systemd:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +0::/system.slice/containerd.service +''' # noqa: E501 + with _mock_open(docker_cgroup_example): + assert docker._is_in_docker() is True + + +def test_in_docker_docker_not_in_file(): + non_docker_cgroup_example = b'''\ +12:perf_event:/ +11:hugetlb:/ +10:devices:/ +9:blkio:/ +8:rdma:/ +7:cpuset:/ +6:cpu,cpuacct:/ +5:freezer:/ +4:memory:/ +3:pids:/ +2:net_cls,net_prio:/ +1:name=systemd:/init.scope +0::/init.scope +''' + with _mock_open(non_docker_cgroup_example): + assert docker._is_in_docker() is False + + +def test_get_docker_path_not_in_docker_returns_same(): + with mock.patch.object(docker, '_is_in_docker', return_value=False): + assert docker._get_docker_path('abc') == 'abc' + + +@pytest.fixture +def in_docker(): + with mock.patch.object(docker, '_is_in_docker', return_value=True): + yield + + +def _linux_commonpath(): + return mock.patch.object(os.path, 'commonpath', posixpath.commonpath) + + +def _nt_commonpath(): + return mock.patch.object(os.path, 'commonpath', ntpath.commonpath) + + +def _docker_output(out): + ret = (0, out, b'') + return mock.patch.object(docker, 'cmd_output_b', return_value=ret) + + +def test_get_docker_path_in_docker_no_binds_same_path(in_docker): + docker_out = json.dumps([{'Mounts': []}]).encode() + + with _docker_output(docker_out): + assert docker._get_docker_path('abc') == 'abc' + + +def test_get_docker_path_in_docker_binds_path_equal(in_docker): + binds_list = [{'Source': '/opt/my_code', 'Destination': '/project'}] + docker_out = json.dumps([{'Mounts': binds_list}]).encode() + + with _linux_commonpath(), _docker_output(docker_out): + assert docker._get_docker_path('/project') == '/opt/my_code' + + +def test_get_docker_path_in_docker_binds_path_complex(in_docker): + binds_list = [{'Source': '/opt/my_code', 'Destination': '/project'}] + docker_out = json.dumps([{'Mounts': binds_list}]).encode() + + with _linux_commonpath(), _docker_output(docker_out): + path = '/project/test/something' + assert docker._get_docker_path(path) == '/opt/my_code/test/something' + + +def test_get_docker_path_in_docker_no_substring(in_docker): + binds_list = [{'Source': '/opt/my_code', 'Destination': '/project'}] + docker_out = json.dumps([{'Mounts': binds_list}]).encode() + + with _linux_commonpath(), _docker_output(docker_out): + path = '/projectSuffix/test/something' + assert docker._get_docker_path(path) == path + + +def test_get_docker_path_in_docker_binds_path_many_binds(in_docker): + binds_list = [ + {'Source': '/something_random', 'Destination': '/not-related'}, + {'Source': '/opt/my_code', 'Destination': '/project'}, + {'Source': '/something-random-2', 'Destination': '/not-related-2'}, + ] + docker_out = json.dumps([{'Mounts': binds_list}]).encode() + + with _linux_commonpath(), _docker_output(docker_out): + assert docker._get_docker_path('/project') == '/opt/my_code' + + +def test_get_docker_path_in_docker_windows(in_docker): + binds_list = [{'Source': r'c:\users\user', 'Destination': r'c:\folder'}] + docker_out = json.dumps([{'Mounts': binds_list}]).encode() + + with _nt_commonpath(), _docker_output(docker_out): + path = r'c:\folder\test\something' + expected = r'c:\users\user\test\something' + assert docker._get_docker_path(path) == expected From a19a59652f736deb43b6c150fb8e33eb3bb04aa2 Mon Sep 17 00:00:00 2001 From: Lorenz Date: Mon, 3 May 2021 18:08:22 +0200 Subject: [PATCH 511/967] Use more common package definition --- pre_commit/languages/r.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 83e600094..64234c1d1 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -107,11 +107,13 @@ def install_environment( 'renv::activate("', file.path(getwd()), '"); ' ) writeLines(activate_statement, 'activate.R') - is_package <- tryCatch({{ - content_desc <- read.dcf(file.path(prefix_dir, 'DESCRIPTION')) - suppressWarnings(unname(content_desc[,'Type']) == "Package") - }}, - error = function(...) FALSE + is_package <- tryCatch( + {{ + path_desc <- file.path(prefix_dir, 'DESCRIPTION') + suppressWarnings(desc <- read.dcf(path_desc)) + "Package" %in% colnames(desc) + }}, + error = function(...) FALSE ) if (is_package) {{ renv::install(prefix_dir) From 6485dd45a33953f6fa23a32e35b5c78c4c65aa4c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 May 2021 17:18:16 +0000 Subject: [PATCH 512/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-autopep8: v1.5.6 → v1.5.7](https://github.com/pre-commit/mirrors-autopep8/compare/v1.5.6...v1.5.7) - [github.com/asottile/pyupgrade: v2.13.0 → v2.14.0](https://github.com/asottile/pyupgrade/compare/v2.13.0...v2.14.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6438bf835..329137527 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: flake8 additional_dependencies: [flake8-typing-imports==1.10.0] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.5.6 + rev: v1.5.7 hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.13.0 + rev: v2.14.0 hooks: - id: pyupgrade args: [--py36-plus] From b8fff8c508b79f70d19a00b7a748effdbf430507 Mon Sep 17 00:00:00 2001 From: Lorenz Date: Tue, 9 Mar 2021 09:28:47 +0100 Subject: [PATCH 513/967] Avoid warnings with R hooks when renv version don't match --- pre_commit/languages/r.py | 30 +- .../resources/empty_template_LICENSE.renv | 7 + .../resources/empty_template_activate.R | 440 ++++++++++++++++++ pre_commit/store.py | 11 +- testing/resources/r_hooks_repo/renv/LICENSE | 7 + .../resources/r_hooks_repo/renv/activate.R | 440 ++++++++++++++++++ tests/store_test.py | 2 +- 7 files changed, 919 insertions(+), 18 deletions(-) create mode 100644 pre_commit/resources/empty_template_LICENSE.renv create mode 100644 pre_commit/resources/empty_template_activate.R create mode 100644 testing/resources/r_hooks_repo/renv/LICENSE create mode 100644 testing/resources/r_hooks_repo/renv/activate.R diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 83e600094..fb789f9d4 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -15,6 +15,7 @@ from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'renv' +RSCRIPT_OPTS = ('--no-save', '--no-restore', '--no-site-file', '--no-environ') get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy @@ -69,12 +70,11 @@ def _entry_validate(entry: Sequence[str]) -> None: def _cmd_from_hook(hook: Hook) -> Tuple[str, ...]: - opts = ('--no-save', '--no-restore', '--no-site-file', '--no-environ') entry = shlex.split(hook.entry) _entry_validate(entry) return ( - *entry[:1], *opts, + *entry[:1], *RSCRIPT_OPTS, *_prefix_if_file_entry(entry, hook.prefix), *hook.args, ) @@ -89,22 +89,23 @@ def install_environment( with clean_path_on_failure(env_dir): os.makedirs(env_dir, exist_ok=True) shutil.copy(prefix.path('renv.lock'), env_dir) + shutil.copytree(prefix.path('renv'), os.path.join(env_dir, 'renv')) cmd_output_b( 'Rscript', '--vanilla', '-e', f"""\ prefix_dir <- {prefix.prefix_dir!r} - missing_pkgs <- setdiff( - "renv", unname(installed.packages()[, "Package"]) - ) options( repos = c(CRAN = "https://cran.rstudio.com"), renv.consent = TRUE ) - install.packages(missing_pkgs) - renv::activate() + source("renv/activate.R") renv::restore() activate_statement <- paste0( - 'renv::activate("', file.path(getwd()), '"); ' + 'suppressWarnings({{', + 'old <- setwd("', getwd(), '"); ', + 'source("renv/activate.R"); ', + 'setwd(old); ', + 'renv::load("', getwd(), '");}})' ) writeLines(activate_statement, 'activate.R') is_package <- tryCatch({{ @@ -120,12 +121,13 @@ def install_environment( cwd=env_dir, ) if additional_dependencies: - cmd_output_b( - 'Rscript', '-e', - 'renv::install(commandArgs(trailingOnly = TRUE))', - *additional_dependencies, - cwd=env_dir, - ) + with in_env(prefix, version): + cmd_output_b( + 'Rscript', *RSCRIPT_OPTS, '-e', + 'renv::install(commandArgs(trailingOnly = TRUE))', + *additional_dependencies, + cwd=env_dir, + ) def run_hook( diff --git a/pre_commit/resources/empty_template_LICENSE.renv b/pre_commit/resources/empty_template_LICENSE.renv new file mode 100644 index 000000000..253c5d1ab --- /dev/null +++ b/pre_commit/resources/empty_template_LICENSE.renv @@ -0,0 +1,7 @@ +Copyright 2021 RStudio, PBC + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/pre_commit/resources/empty_template_activate.R b/pre_commit/resources/empty_template_activate.R new file mode 100644 index 000000000..d8d092cc6 --- /dev/null +++ b/pre_commit/resources/empty_template_activate.R @@ -0,0 +1,440 @@ + +local({ + + # the requested version of renv + version <- "0.12.5" + + # the project directory + project <- getwd() + + # avoid recursion + if (!is.na(Sys.getenv("RENV_R_INITIALIZING", unset = NA))) + return(invisible(TRUE)) + + # signal that we're loading renv during R startup + Sys.setenv("RENV_R_INITIALIZING" = "true") + on.exit(Sys.unsetenv("RENV_R_INITIALIZING"), add = TRUE) + + # signal that we've consented to use renv + options(renv.consent = TRUE) + + # load the 'utils' package eagerly -- this ensures that renv shims, which + # mask 'utils' packages, will come first on the search path + library(utils, lib.loc = .Library) + + # check to see if renv has already been loaded + if ("renv" %in% loadedNamespaces()) { + + # if renv has already been loaded, and it's the requested version of renv, + # nothing to do + spec <- .getNamespaceInfo(.getNamespace("renv"), "spec") + if (identical(spec[["version"]], version)) + return(invisible(TRUE)) + + # otherwise, unload and attempt to load the correct version of renv + unloadNamespace("renv") + + } + + # load bootstrap tools + bootstrap <- function(version, library) { + + # attempt to download renv + tarball <- tryCatch(renv_bootstrap_download(version), error = identity) + if (inherits(tarball, "error")) + stop("failed to download renv ", version) + + # now attempt to install + status <- tryCatch(renv_bootstrap_install(version, tarball, library), error = identity) + if (inherits(status, "error")) + stop("failed to install renv ", version) + + } + + renv_bootstrap_tests_running <- function() { + getOption("renv.tests.running", default = FALSE) + } + + renv_bootstrap_repos <- function() { + + # check for repos override + repos <- Sys.getenv("RENV_CONFIG_REPOS_OVERRIDE", unset = NA) + if (!is.na(repos)) + return(repos) + + # if we're testing, re-use the test repositories + if (renv_bootstrap_tests_running()) + return(getOption("renv.tests.repos")) + + # retrieve current repos + repos <- getOption("repos") + + # ensure @CRAN@ entries are resolved + repos[repos == "@CRAN@"] <- "https://cloud.r-project.org" + + # add in renv.bootstrap.repos if set + default <- c(CRAN = "https://cloud.r-project.org") + extra <- getOption("renv.bootstrap.repos", default = default) + repos <- c(repos, extra) + + # remove duplicates that might've snuck in + dupes <- duplicated(repos) | duplicated(names(repos)) + repos[!dupes] + + } + + renv_bootstrap_download <- function(version) { + + # if the renv version number has 4 components, assume it must + # be retrieved via github + nv <- numeric_version(version) + components <- unclass(nv)[[1]] + + methods <- if (length(components) == 4L) { + list( + renv_bootstrap_download_github + ) + } else { + list( + renv_bootstrap_download_cran_latest, + renv_bootstrap_download_cran_archive + ) + } + + for (method in methods) { + path <- tryCatch(method(version), error = identity) + if (is.character(path) && file.exists(path)) + return(path) + } + + stop("failed to download renv ", version) + + } + + renv_bootstrap_download_impl <- function(url, destfile) { + + mode <- "wb" + + # https://bugs.r-project.org/bugzilla/show_bug.cgi?id=17715 + fixup <- + Sys.info()[["sysname"]] == "Windows" && + substring(url, 1L, 5L) == "file:" + + if (fixup) + mode <- "w+b" + + utils::download.file( + url = url, + destfile = destfile, + mode = mode, + quiet = TRUE + ) + + } + + renv_bootstrap_download_cran_latest <- function(version) { + + repos <- renv_bootstrap_download_cran_latest_find(version) + + message("* Downloading renv ", version, " from CRAN ... ", appendLF = FALSE) + + info <- tryCatch( + utils::download.packages( + pkgs = "renv", + repos = repos, + destdir = tempdir(), + quiet = TRUE + ), + condition = identity + ) + + if (inherits(info, "condition")) { + message("FAILED") + return(FALSE) + } + + message("OK") + info[1, 2] + + } + + renv_bootstrap_download_cran_latest_find <- function(version) { + + all <- renv_bootstrap_repos() + + for (repos in all) { + + db <- tryCatch( + as.data.frame( + x = utils::available.packages(repos = repos), + stringsAsFactors = FALSE + ), + error = identity + ) + + if (inherits(db, "error")) + next + + entry <- db[db$Package %in% "renv" & db$Version %in% version, ] + if (nrow(entry) == 0) + next + + return(repos) + + } + + fmt <- "renv %s is not available from your declared package repositories" + stop(sprintf(fmt, version)) + + } + + renv_bootstrap_download_cran_archive <- function(version) { + + name <- sprintf("renv_%s.tar.gz", version) + repos <- renv_bootstrap_repos() + urls <- file.path(repos, "src/contrib/Archive/renv", name) + destfile <- file.path(tempdir(), name) + + message("* Downloading renv ", version, " from CRAN archive ... ", appendLF = FALSE) + + for (url in urls) { + + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (identical(status, 0L)) { + message("OK") + return(destfile) + } + + } + + message("FAILED") + return(FALSE) + + } + + renv_bootstrap_download_github <- function(version) { + + enabled <- Sys.getenv("RENV_BOOTSTRAP_FROM_GITHUB", unset = "TRUE") + if (!identical(enabled, "TRUE")) + return(FALSE) + + # prepare download options + pat <- Sys.getenv("GITHUB_PAT") + if (nzchar(Sys.which("curl")) && nzchar(pat)) { + fmt <- "--location --fail --header \"Authorization: token %s\"" + extra <- sprintf(fmt, pat) + saved <- options("download.file.method", "download.file.extra") + options(download.file.method = "curl", download.file.extra = extra) + on.exit(do.call(base::options, saved), add = TRUE) + } else if (nzchar(Sys.which("wget")) && nzchar(pat)) { + fmt <- "--header=\"Authorization: token %s\"" + extra <- sprintf(fmt, pat) + saved <- options("download.file.method", "download.file.extra") + options(download.file.method = "wget", download.file.extra = extra) + on.exit(do.call(base::options, saved), add = TRUE) + } + + message("* Downloading renv ", version, " from GitHub ... ", appendLF = FALSE) + + url <- file.path("https://api.github.com/repos/rstudio/renv/tarball", version) + name <- sprintf("renv_%s.tar.gz", version) + destfile <- file.path(tempdir(), name) + + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (!identical(status, 0L)) { + message("FAILED") + return(FALSE) + } + + message("OK") + return(destfile) + + } + + renv_bootstrap_install <- function(version, tarball, library) { + + # attempt to install it into project library + message("* Installing renv ", version, " ... ", appendLF = FALSE) + dir.create(library, showWarnings = FALSE, recursive = TRUE) + + # invoke using system2 so we can capture and report output + bin <- R.home("bin") + exe <- if (Sys.info()[["sysname"]] == "Windows") "R.exe" else "R" + r <- file.path(bin, exe) + args <- c("--vanilla", "CMD", "INSTALL", "-l", shQuote(library), shQuote(tarball)) + output <- system2(r, args, stdout = TRUE, stderr = TRUE) + message("Done!") + + # check for successful install + status <- attr(output, "status") + if (is.numeric(status) && !identical(status, 0L)) { + header <- "Error installing renv:" + lines <- paste(rep.int("=", nchar(header)), collapse = "") + text <- c(header, lines, output) + writeLines(text, con = stderr()) + } + + status + + } + + renv_bootstrap_prefix <- function() { + + # construct version prefix + version <- paste(R.version$major, R.version$minor, sep = ".") + prefix <- paste("R", numeric_version(version)[1, 1:2], sep = "-") + + # include SVN revision for development versions of R + # (to avoid sharing platform-specific artefacts with released versions of R) + devel <- + identical(R.version[["status"]], "Under development (unstable)") || + identical(R.version[["nickname"]], "Unsuffered Consequences") + + if (devel) + prefix <- paste(prefix, R.version[["svn rev"]], sep = "-r") + + # build list of path components + components <- c(prefix, R.version$platform) + + # include prefix if provided by user + prefix <- Sys.getenv("RENV_PATHS_PREFIX") + if (nzchar(prefix)) + components <- c(prefix, components) + + # build prefix + paste(components, collapse = "/") + + } + + renv_bootstrap_library_root_name <- function(project) { + + # use project name as-is if requested + asis <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT_ASIS", unset = "FALSE") + if (asis) + return(basename(project)) + + # otherwise, disambiguate based on project's path + id <- substring(renv_bootstrap_hash_text(project), 1L, 8L) + paste(basename(project), id, sep = "-") + + } + + renv_bootstrap_library_root <- function(project) { + + path <- Sys.getenv("RENV_PATHS_LIBRARY", unset = NA) + if (!is.na(path)) + return(path) + + path <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT", unset = NA) + if (!is.na(path)) { + name <- renv_bootstrap_library_root_name(project) + return(file.path(path, name)) + } + + file.path(project, "renv/library") + + } + + renv_bootstrap_validate_version <- function(version) { + + loadedversion <- utils::packageDescription("renv", fields = "Version") + if (version == loadedversion) + return(TRUE) + + # assume four-component versions are from GitHub; three-component + # versions are from CRAN + components <- strsplit(loadedversion, "[.-]")[[1]] + remote <- if (length(components) == 4L) + paste("rstudio/renv", loadedversion, sep = "@") + else + paste("renv", loadedversion, sep = "@") + + fmt <- paste( + "renv %1$s was loaded from project library, but this project is configured to use renv %2$s.", + "Use `renv::record(\"%3$s\")` to record renv %1$s in the lockfile.", + "Use `renv::restore(packages = \"renv\")` to install renv %2$s into the project library.", + sep = "\n" + ) + + msg <- sprintf(fmt, loadedversion, version, remote) + warning(msg, call. = FALSE) + + FALSE + + } + + renv_bootstrap_hash_text <- function(text) { + + hashfile <- tempfile("renv-hash-") + on.exit(unlink(hashfile), add = TRUE) + + writeLines(text, con = hashfile) + tools::md5sum(hashfile) + + } + + renv_bootstrap_load <- function(project, libpath, version) { + + # try to load renv from the project library + if (!requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) + return(FALSE) + + # warn if the version of renv loaded does not match + renv_bootstrap_validate_version(version) + + # load the project + renv::load(project) + + TRUE + + } + + # construct path to library root + root <- renv_bootstrap_library_root(project) + + # construct library prefix for platform + prefix <- renv_bootstrap_prefix() + + # construct full libpath + libpath <- file.path(root, prefix) + + # attempt to load + if (renv_bootstrap_load(project, libpath, version)) + return(TRUE) + + # load failed; inform user we're about to bootstrap + prefix <- paste("# Bootstrapping renv", version) + postfix <- paste(rep.int("-", 77L - nchar(prefix)), collapse = "") + header <- paste(prefix, postfix) + message(header) + + # perform bootstrap + bootstrap(version, libpath) + + # exit early if we're just testing bootstrap + if (!is.na(Sys.getenv("RENV_BOOTSTRAP_INSTALL_ONLY", unset = NA))) + return(TRUE) + + # try again to load + if (requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) { + message("* Successfully installed and loaded renv ", version, ".") + return(renv::load()) + } + + # failed to download or load renv; warn the user + msg <- c( + "Failed to find an renv installation: the project will not be loaded.", + "Use `renv::activate()` to re-initialize the project." + ) + + warning(paste(msg, collapse = "\n"), call. = FALSE) + +}) diff --git a/pre_commit/store.py b/pre_commit/store.py index 187c9d353..494e688f1 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -189,14 +189,19 @@ def _git_cmd(*args: str) -> None: LOCAL_RESOURCES = ( 'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore', 'package.json', 'pre_commit_dummy_package.gemspec', 'setup.py', - 'environment.yml', 'Makefile.PL', 'renv.lock', + 'environment.yml', 'Makefile.PL', + 'renv.lock', 'renv/activate.R', 'renv/LICENSE.renv', ) def make_local(self, deps: Sequence[str]) -> str: def make_local_strategy(directory: str) -> None: for resource in self.LOCAL_RESOURCES: - contents = resource_text(f'empty_template_{resource}') - with open(os.path.join(directory, resource), 'w') as f: + resource_dirname, resource_basename = os.path.split(resource) + contents = resource_text(f'empty_template_{resource_basename}') + target_dir = os.path.join(directory, resource_dirname) + target_file = os.path.join(target_dir, resource_basename) + os.makedirs(target_dir, exist_ok=True) + with open(target_file, 'w') as f: f.write(contents) env = git.no_git_env() diff --git a/testing/resources/r_hooks_repo/renv/LICENSE b/testing/resources/r_hooks_repo/renv/LICENSE new file mode 100644 index 000000000..253c5d1ab --- /dev/null +++ b/testing/resources/r_hooks_repo/renv/LICENSE @@ -0,0 +1,7 @@ +Copyright 2021 RStudio, PBC + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/testing/resources/r_hooks_repo/renv/activate.R b/testing/resources/r_hooks_repo/renv/activate.R new file mode 100644 index 000000000..d8d092cc6 --- /dev/null +++ b/testing/resources/r_hooks_repo/renv/activate.R @@ -0,0 +1,440 @@ + +local({ + + # the requested version of renv + version <- "0.12.5" + + # the project directory + project <- getwd() + + # avoid recursion + if (!is.na(Sys.getenv("RENV_R_INITIALIZING", unset = NA))) + return(invisible(TRUE)) + + # signal that we're loading renv during R startup + Sys.setenv("RENV_R_INITIALIZING" = "true") + on.exit(Sys.unsetenv("RENV_R_INITIALIZING"), add = TRUE) + + # signal that we've consented to use renv + options(renv.consent = TRUE) + + # load the 'utils' package eagerly -- this ensures that renv shims, which + # mask 'utils' packages, will come first on the search path + library(utils, lib.loc = .Library) + + # check to see if renv has already been loaded + if ("renv" %in% loadedNamespaces()) { + + # if renv has already been loaded, and it's the requested version of renv, + # nothing to do + spec <- .getNamespaceInfo(.getNamespace("renv"), "spec") + if (identical(spec[["version"]], version)) + return(invisible(TRUE)) + + # otherwise, unload and attempt to load the correct version of renv + unloadNamespace("renv") + + } + + # load bootstrap tools + bootstrap <- function(version, library) { + + # attempt to download renv + tarball <- tryCatch(renv_bootstrap_download(version), error = identity) + if (inherits(tarball, "error")) + stop("failed to download renv ", version) + + # now attempt to install + status <- tryCatch(renv_bootstrap_install(version, tarball, library), error = identity) + if (inherits(status, "error")) + stop("failed to install renv ", version) + + } + + renv_bootstrap_tests_running <- function() { + getOption("renv.tests.running", default = FALSE) + } + + renv_bootstrap_repos <- function() { + + # check for repos override + repos <- Sys.getenv("RENV_CONFIG_REPOS_OVERRIDE", unset = NA) + if (!is.na(repos)) + return(repos) + + # if we're testing, re-use the test repositories + if (renv_bootstrap_tests_running()) + return(getOption("renv.tests.repos")) + + # retrieve current repos + repos <- getOption("repos") + + # ensure @CRAN@ entries are resolved + repos[repos == "@CRAN@"] <- "https://cloud.r-project.org" + + # add in renv.bootstrap.repos if set + default <- c(CRAN = "https://cloud.r-project.org") + extra <- getOption("renv.bootstrap.repos", default = default) + repos <- c(repos, extra) + + # remove duplicates that might've snuck in + dupes <- duplicated(repos) | duplicated(names(repos)) + repos[!dupes] + + } + + renv_bootstrap_download <- function(version) { + + # if the renv version number has 4 components, assume it must + # be retrieved via github + nv <- numeric_version(version) + components <- unclass(nv)[[1]] + + methods <- if (length(components) == 4L) { + list( + renv_bootstrap_download_github + ) + } else { + list( + renv_bootstrap_download_cran_latest, + renv_bootstrap_download_cran_archive + ) + } + + for (method in methods) { + path <- tryCatch(method(version), error = identity) + if (is.character(path) && file.exists(path)) + return(path) + } + + stop("failed to download renv ", version) + + } + + renv_bootstrap_download_impl <- function(url, destfile) { + + mode <- "wb" + + # https://bugs.r-project.org/bugzilla/show_bug.cgi?id=17715 + fixup <- + Sys.info()[["sysname"]] == "Windows" && + substring(url, 1L, 5L) == "file:" + + if (fixup) + mode <- "w+b" + + utils::download.file( + url = url, + destfile = destfile, + mode = mode, + quiet = TRUE + ) + + } + + renv_bootstrap_download_cran_latest <- function(version) { + + repos <- renv_bootstrap_download_cran_latest_find(version) + + message("* Downloading renv ", version, " from CRAN ... ", appendLF = FALSE) + + info <- tryCatch( + utils::download.packages( + pkgs = "renv", + repos = repos, + destdir = tempdir(), + quiet = TRUE + ), + condition = identity + ) + + if (inherits(info, "condition")) { + message("FAILED") + return(FALSE) + } + + message("OK") + info[1, 2] + + } + + renv_bootstrap_download_cran_latest_find <- function(version) { + + all <- renv_bootstrap_repos() + + for (repos in all) { + + db <- tryCatch( + as.data.frame( + x = utils::available.packages(repos = repos), + stringsAsFactors = FALSE + ), + error = identity + ) + + if (inherits(db, "error")) + next + + entry <- db[db$Package %in% "renv" & db$Version %in% version, ] + if (nrow(entry) == 0) + next + + return(repos) + + } + + fmt <- "renv %s is not available from your declared package repositories" + stop(sprintf(fmt, version)) + + } + + renv_bootstrap_download_cran_archive <- function(version) { + + name <- sprintf("renv_%s.tar.gz", version) + repos <- renv_bootstrap_repos() + urls <- file.path(repos, "src/contrib/Archive/renv", name) + destfile <- file.path(tempdir(), name) + + message("* Downloading renv ", version, " from CRAN archive ... ", appendLF = FALSE) + + for (url in urls) { + + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (identical(status, 0L)) { + message("OK") + return(destfile) + } + + } + + message("FAILED") + return(FALSE) + + } + + renv_bootstrap_download_github <- function(version) { + + enabled <- Sys.getenv("RENV_BOOTSTRAP_FROM_GITHUB", unset = "TRUE") + if (!identical(enabled, "TRUE")) + return(FALSE) + + # prepare download options + pat <- Sys.getenv("GITHUB_PAT") + if (nzchar(Sys.which("curl")) && nzchar(pat)) { + fmt <- "--location --fail --header \"Authorization: token %s\"" + extra <- sprintf(fmt, pat) + saved <- options("download.file.method", "download.file.extra") + options(download.file.method = "curl", download.file.extra = extra) + on.exit(do.call(base::options, saved), add = TRUE) + } else if (nzchar(Sys.which("wget")) && nzchar(pat)) { + fmt <- "--header=\"Authorization: token %s\"" + extra <- sprintf(fmt, pat) + saved <- options("download.file.method", "download.file.extra") + options(download.file.method = "wget", download.file.extra = extra) + on.exit(do.call(base::options, saved), add = TRUE) + } + + message("* Downloading renv ", version, " from GitHub ... ", appendLF = FALSE) + + url <- file.path("https://api.github.com/repos/rstudio/renv/tarball", version) + name <- sprintf("renv_%s.tar.gz", version) + destfile <- file.path(tempdir(), name) + + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (!identical(status, 0L)) { + message("FAILED") + return(FALSE) + } + + message("OK") + return(destfile) + + } + + renv_bootstrap_install <- function(version, tarball, library) { + + # attempt to install it into project library + message("* Installing renv ", version, " ... ", appendLF = FALSE) + dir.create(library, showWarnings = FALSE, recursive = TRUE) + + # invoke using system2 so we can capture and report output + bin <- R.home("bin") + exe <- if (Sys.info()[["sysname"]] == "Windows") "R.exe" else "R" + r <- file.path(bin, exe) + args <- c("--vanilla", "CMD", "INSTALL", "-l", shQuote(library), shQuote(tarball)) + output <- system2(r, args, stdout = TRUE, stderr = TRUE) + message("Done!") + + # check for successful install + status <- attr(output, "status") + if (is.numeric(status) && !identical(status, 0L)) { + header <- "Error installing renv:" + lines <- paste(rep.int("=", nchar(header)), collapse = "") + text <- c(header, lines, output) + writeLines(text, con = stderr()) + } + + status + + } + + renv_bootstrap_prefix <- function() { + + # construct version prefix + version <- paste(R.version$major, R.version$minor, sep = ".") + prefix <- paste("R", numeric_version(version)[1, 1:2], sep = "-") + + # include SVN revision for development versions of R + # (to avoid sharing platform-specific artefacts with released versions of R) + devel <- + identical(R.version[["status"]], "Under development (unstable)") || + identical(R.version[["nickname"]], "Unsuffered Consequences") + + if (devel) + prefix <- paste(prefix, R.version[["svn rev"]], sep = "-r") + + # build list of path components + components <- c(prefix, R.version$platform) + + # include prefix if provided by user + prefix <- Sys.getenv("RENV_PATHS_PREFIX") + if (nzchar(prefix)) + components <- c(prefix, components) + + # build prefix + paste(components, collapse = "/") + + } + + renv_bootstrap_library_root_name <- function(project) { + + # use project name as-is if requested + asis <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT_ASIS", unset = "FALSE") + if (asis) + return(basename(project)) + + # otherwise, disambiguate based on project's path + id <- substring(renv_bootstrap_hash_text(project), 1L, 8L) + paste(basename(project), id, sep = "-") + + } + + renv_bootstrap_library_root <- function(project) { + + path <- Sys.getenv("RENV_PATHS_LIBRARY", unset = NA) + if (!is.na(path)) + return(path) + + path <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT", unset = NA) + if (!is.na(path)) { + name <- renv_bootstrap_library_root_name(project) + return(file.path(path, name)) + } + + file.path(project, "renv/library") + + } + + renv_bootstrap_validate_version <- function(version) { + + loadedversion <- utils::packageDescription("renv", fields = "Version") + if (version == loadedversion) + return(TRUE) + + # assume four-component versions are from GitHub; three-component + # versions are from CRAN + components <- strsplit(loadedversion, "[.-]")[[1]] + remote <- if (length(components) == 4L) + paste("rstudio/renv", loadedversion, sep = "@") + else + paste("renv", loadedversion, sep = "@") + + fmt <- paste( + "renv %1$s was loaded from project library, but this project is configured to use renv %2$s.", + "Use `renv::record(\"%3$s\")` to record renv %1$s in the lockfile.", + "Use `renv::restore(packages = \"renv\")` to install renv %2$s into the project library.", + sep = "\n" + ) + + msg <- sprintf(fmt, loadedversion, version, remote) + warning(msg, call. = FALSE) + + FALSE + + } + + renv_bootstrap_hash_text <- function(text) { + + hashfile <- tempfile("renv-hash-") + on.exit(unlink(hashfile), add = TRUE) + + writeLines(text, con = hashfile) + tools::md5sum(hashfile) + + } + + renv_bootstrap_load <- function(project, libpath, version) { + + # try to load renv from the project library + if (!requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) + return(FALSE) + + # warn if the version of renv loaded does not match + renv_bootstrap_validate_version(version) + + # load the project + renv::load(project) + + TRUE + + } + + # construct path to library root + root <- renv_bootstrap_library_root(project) + + # construct library prefix for platform + prefix <- renv_bootstrap_prefix() + + # construct full libpath + libpath <- file.path(root, prefix) + + # attempt to load + if (renv_bootstrap_load(project, libpath, version)) + return(TRUE) + + # load failed; inform user we're about to bootstrap + prefix <- paste("# Bootstrapping renv", version) + postfix <- paste(rep.int("-", 77L - nchar(prefix)), collapse = "") + header <- paste(prefix, postfix) + message(header) + + # perform bootstrap + bootstrap(version, libpath) + + # exit early if we're just testing bootstrap + if (!is.na(Sys.getenv("RENV_BOOTSTRAP_INSTALL_ONLY", unset = NA))) + return(TRUE) + + # try again to load + if (requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) { + message("* Successfully installed and loaded renv ", version, ".") + return(renv::load()) + } + + # failed to download or load renv; warn the user + msg <- c( + "Failed to find an renv installation: the project will not be loaded.", + "Use `renv::activate()` to re-initialize the project." + ) + + warning(paste(msg, collapse = "\n"), call. = FALSE) + +}) diff --git a/tests/store_test.py b/tests/store_test.py index 0947144ed..5a5d69e0f 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -186,7 +186,7 @@ def test_local_resources_reflects_reality(): for res in os.listdir('pre_commit/resources') if res.startswith('empty_template_') } - assert on_disk == set(Store.LOCAL_RESOURCES) + assert on_disk == {os.path.basename(x) for x in Store.LOCAL_RESOURCES} def test_mark_config_as_used(store, tmpdir): From 788aec156f6246dcb0052aaeb2494d9e5b786d71 Mon Sep 17 00:00:00 2001 From: Lorenz Date: Thu, 15 Apr 2021 08:03:37 +0200 Subject: [PATCH 514/967] local r hooks should not get prefix for path --- pre_commit/languages/r.py | 11 ++++++++--- tests/languages/r_test.py | 29 +++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index bae4001aa..d573775f7 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -40,14 +40,19 @@ def _get_env_dir(prefix: Prefix, version: str) -> str: return prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) -def _prefix_if_file_entry( +def _prefix_if_non_local_file_entry( entry: Sequence[str], prefix: Prefix, + src: str, ) -> Sequence[str]: if entry[1] == '-e': return entry[1:] else: - return (prefix.path(entry[1]),) + if src == 'local': + path = entry[1] + else: + path = prefix.path(entry[1]) + return (path,) def _entry_validate(entry: Sequence[str]) -> None: @@ -75,7 +80,7 @@ def _cmd_from_hook(hook: Hook) -> Tuple[str, ...]: return ( *entry[:1], *RSCRIPT_OPTS, - *_prefix_if_file_entry(entry, hook.prefix), + *_prefix_if_non_local_file_entry(entry, hook.prefix, hook.src), *hook.args, ) diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py index 5c046efed..66aa7b388 100644 --- a/tests/languages/r_test.py +++ b/tests/languages/r_test.py @@ -14,10 +14,12 @@ def _test_r_parsing( hook_id, expected_hook_expr={}, expected_args={}, + config={}, + expect_path_prefix=True, ): repo_path = 'r_hooks_repo' path = make_repo(tempdir_factory, repo_path) - config = make_config_from_repo(path) + config = config or make_config_from_repo(path) hook = _get_hook_no_install(config, store, hook_id) ret = r._cmd_from_hook(hook) expected_cmd = 'Rscript' @@ -25,7 +27,8 @@ def _test_r_parsing( '--no-save', '--no-restore', '--no-site-file', '--no-environ', ) expected_path = os.path.join( - hook.prefix.prefix_dir, '.'.join([hook_id, 'R']), + hook.prefix.prefix_dir if expect_path_prefix else '', + f'{hook_id}.R', ) expected = ( expected_cmd, @@ -102,3 +105,25 @@ def test_r_parsing_expr_non_Rscirpt(tempdir_factory, store): msg = execinfo.value.args assert msg == ('entry must start with `Rscript`.',) + + +def test_r_parsing_file_local(tempdir_factory, store): + path = 'path/to/script.R' + hook_id = 'local-r' + config = { + 'repo': 'local', + 'hooks': [{ + 'id': hook_id, + 'name': 'local-r', + 'entry': f'Rscript {path}', + 'language': 'r', + }], + } + _test_r_parsing( + tempdir_factory, + store, + hook_id=hook_id, + expected_hook_expr=(path,), + config=config, + expect_path_prefix=False, + ) From b9c2c477ccd2bacffee763fa2bd205b011e3e07c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 May 2021 20:19:33 +0000 Subject: [PATCH 515/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 3.9.1 → 3.9.2](https://github.com/PyCQA/flake8/compare/3.9.1...3.9.2) - [github.com/asottile/pyupgrade: v2.14.0 → v2.15.0](https://github.com/asottile/pyupgrade/compare/v2.14.0...v2.15.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 329137527..ffb5b6580 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: - id: requirements-txt-fixer - id: double-quote-string-fixer - repo: https://github.com/PyCQA/flake8 - rev: 3.9.1 + rev: 3.9.2 hooks: - id: flake8 additional_dependencies: [flake8-typing-imports==1.10.0] @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.14.0 + rev: v2.15.0 hooks: - id: pyupgrade args: [--py36-plus] From 3922263f8c7e459379bd376d744b07bf1c74d8e7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 14 May 2021 19:11:05 -0700 Subject: [PATCH 516/967] Use more inclusive language Committed via https://github.com/asottile/all-repos --- CHANGELOG.md | 2 +- pre_commit/resources/empty_template_Makefile.PL | 2 +- pre_commit/resources/empty_template_go.mod | 2 +- pre_commit/resources/empty_template_package.json | 2 +- ...empty_template_pre_commit_dummy_package.gemspec | 6 ------ ...template_pre_commit_placeholder_package.gemspec | 6 ++++++ pre_commit/resources/empty_template_setup.py | 2 +- pre_commit/store.py | 2 +- tests/commands/run_test.py | 14 +++++++------- tests/conftest.py | 4 ++-- tests/languages/ruby_test.py | 8 ++++---- 11 files changed, 25 insertions(+), 25 deletions(-) delete mode 100644 pre_commit/resources/empty_template_pre_commit_dummy_package.gemspec create mode 100644 pre_commit/resources/empty_template_pre_commit_placeholder_package.gemspec diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f154c447..50492dfb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1444,7 +1444,7 @@ that have helped us get this far! 0.13.1 - 2017-02-16 =================== -- Fix dummy gem for ruby local hooks +- Fix placeholder gem for ruby local hooks 0.13.0 - 2017-02-16 =================== diff --git a/pre_commit/resources/empty_template_Makefile.PL b/pre_commit/resources/empty_template_Makefile.PL index ac75fe531..45a0ba377 100644 --- a/pre_commit/resources/empty_template_Makefile.PL +++ b/pre_commit/resources/empty_template_Makefile.PL @@ -1,6 +1,6 @@ use ExtUtils::MakeMaker; WriteMakefile( - NAME => "PreCommitDummy", + NAME => "PreCommitPlaceholder", VERSION => "0.0.1", ); diff --git a/pre_commit/resources/empty_template_go.mod b/pre_commit/resources/empty_template_go.mod index de3e24157..892c4e59d 100644 --- a/pre_commit/resources/empty_template_go.mod +++ b/pre_commit/resources/empty_template_go.mod @@ -1 +1 @@ -module pre-commit-dummy-empty-module +module pre-commit-placeholder-empty-module diff --git a/pre_commit/resources/empty_template_package.json b/pre_commit/resources/empty_template_package.json index ac7b72592..042e9583c 100644 --- a/pre_commit/resources/empty_template_package.json +++ b/pre_commit/resources/empty_template_package.json @@ -1,4 +1,4 @@ { - "name": "pre_commit_dummy_package", + "name": "pre_commit_placeholder_package", "version": "0.0.0" } diff --git a/pre_commit/resources/empty_template_pre_commit_dummy_package.gemspec b/pre_commit/resources/empty_template_pre_commit_dummy_package.gemspec deleted file mode 100644 index 8bfb40cad..000000000 --- a/pre_commit/resources/empty_template_pre_commit_dummy_package.gemspec +++ /dev/null @@ -1,6 +0,0 @@ -Gem::Specification.new do |s| - s.name = 'pre_commit_dummy_package' - s.version = '0.0.0' - s.summary = 'dummy gem for pre-commit hooks' - s.authors = ['Anthony Sottile'] -end diff --git a/pre_commit/resources/empty_template_pre_commit_placeholder_package.gemspec b/pre_commit/resources/empty_template_pre_commit_placeholder_package.gemspec new file mode 100644 index 000000000..630f0d4da --- /dev/null +++ b/pre_commit/resources/empty_template_pre_commit_placeholder_package.gemspec @@ -0,0 +1,6 @@ +Gem::Specification.new do |s| + s.name = 'pre_commit_placeholder_package' + s.version = '0.0.0' + s.summary = 'placeholder gem for pre-commit hooks' + s.authors = ['Anthony Sottile'] +end diff --git a/pre_commit/resources/empty_template_setup.py b/pre_commit/resources/empty_template_setup.py index 68860648b..ef05eef84 100644 --- a/pre_commit/resources/empty_template_setup.py +++ b/pre_commit/resources/empty_template_setup.py @@ -1,4 +1,4 @@ from setuptools import setup -setup(name='pre-commit-dummy-package', version='0.0.0') +setup(name='pre-commit-placeholder-package', version='0.0.0') diff --git a/pre_commit/store.py b/pre_commit/store.py index 494e688f1..0fd5e6238 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -188,7 +188,7 @@ def _git_cmd(*args: str) -> None: LOCAL_RESOURCES = ( 'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore', - 'package.json', 'pre_commit_dummy_package.gemspec', 'setup.py', + 'package.json', 'pre_commit_placeholder_package.gemspec', 'setup.py', 'environment.yml', 'Makefile.PL', 'renv.lock', 'renv/activate.R', 'renv/LICENSE.renv', ) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 8dcb5796b..e184340c2 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -526,9 +526,9 @@ def test_merge_conflict(cap_out, store, in_merge_conflict): def test_merge_conflict_modified(cap_out, store, in_merge_conflict): # Touch another file so we have unstaged non-conflicting things - assert os.path.exists('dummy') - with open('dummy', 'w') as dummy_file: - dummy_file.write('bar\nbaz\n') + assert os.path.exists('placeholder') + with open('placeholder', 'w') as placeholder_file: + placeholder_file.write('bar\nbaz\n') ret, printed = _do_run(cap_out, store, in_merge_conflict, run_opts()) assert ret == 1 @@ -831,9 +831,9 @@ def test_local_hook_passes(cap_out, store, repo_with_passing_hook): } add_config_to_repo(repo_with_passing_hook, config) - with open('dummy.py', 'w') as staged_file: + with open('placeholder.py', 'w') as staged_file: staged_file.write('"""TODO: something"""\n') - cmd_output('git', 'add', 'dummy.py') + cmd_output('git', 'add', 'placeholder.py') _test_run( cap_out, @@ -858,9 +858,9 @@ def test_local_hook_fails(cap_out, store, repo_with_passing_hook): } add_config_to_repo(repo_with_passing_hook, config) - with open('dummy.py', 'w') as staged_file: + with open('placeholder.py', 'w') as staged_file: staged_file.write('"""TODO: something"""\n') - cmd_output('git', 'add', 'dummy.py') + cmd_output('git', 'add', 'placeholder.py') _test_run( cap_out, diff --git a/tests/conftest.py b/tests/conftest.py index b36ce5ac5..f38f9693c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -90,8 +90,8 @@ def _make_conflict(): @pytest.fixture def in_merge_conflict(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - open(os.path.join(path, 'dummy'), 'a').close() - cmd_output('git', 'add', 'dummy', cwd=path) + open(os.path.join(path, 'placeholder'), 'a').close() + cmd_output('git', 'add', 'placeholder', cwd=path) git_commit(msg=in_merge_conflict.__name__, cwd=path) conflict_path = tempdir_factory.get() diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 0c6cfede4..7dff0466b 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -36,13 +36,13 @@ def test_uses_system_if_both_gem_and_ruby_are_available(find_exe_mck): def fake_gem_prefix(tmpdir): gemspec = '''\ Gem::Specification.new do |s| - s.name = 'pre_commit_dummy_package' + s.name = 'pre_commit_placeholder_package' s.version = '0.0.0' - s.summary = 'dummy gem for pre-commit hooks' + s.summary = 'placeholder gem for pre-commit hooks' s.authors = ['Anthony Sottile'] end ''' - tmpdir.join('dummy_gem.gemspec').write(gemspec) + tmpdir.join('placeholder_gem.gemspec').write(gemspec) yield Prefix(tmpdir) @@ -53,7 +53,7 @@ def test_install_ruby_system(fake_gem_prefix): # Should be able to activate and use rbenv install with ruby.in_env(fake_gem_prefix, 'system'): _, out, _ = cmd_output('gem', 'list') - assert 'pre_commit_dummy_package' in out + assert 'pre_commit_placeholder_package' in out @xfailif_windows # pragma: win32 no cover From 7f65d2745dd5c60bf38340d7927192b17a13ecf0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 May 2021 17:22:52 +0000 Subject: [PATCH 517/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v3.4.0 → v4.0.1](https://github.com/pre-commit/pre-commit-hooks/compare/v3.4.0...v4.0.1) - [github.com/asottile/pyupgrade: v2.15.0 → v2.16.0](https://github.com/asottile/pyupgrade/compare/v2.15.0...v2.16.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ffb5b6580..8276e5120 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 + rev: v4.0.1 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.15.0 + rev: v2.16.0 hooks: - id: pyupgrade args: [--py36-plus] From c2108d6d43c46e1286645f489a9285bdc068fd24 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 17 May 2021 20:15:00 -0700 Subject: [PATCH 518/967] make tarfile creation reproducible --- pre_commit/resources/rbenv.tar.gz | Bin 34250 -> 32142 bytes pre_commit/resources/ruby-build.tar.gz | Bin 74218 -> 68010 bytes pre_commit/resources/ruby-download.tar.gz | Bin 5533 -> 5271 bytes testing/make-archives | 36 ++++++++++++++++------ 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/pre_commit/resources/rbenv.tar.gz b/pre_commit/resources/rbenv.tar.gz index 95b5a364dff06d5fc8f1176a9ee477c817942b46..7723448ec5fa447c5a369257fcd768bc58093f6b 100644 GIT binary patch literal 32142 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 V!V{kGglEIg{{j9RBvJs-0su=|IMe_D literal 34250 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 AGXMYp diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz index ef82e2f5f53d7b35b4712e30388dff47391b8143..2d8413643b29c326e7435aebdce8036ad0368f92 100644 GIT binary patch literal 68010 zcmV(xK4RJbYXG;?7iz+8^^XNI{)r|ib{hg;Lws< zcae|;gKV>cfdx312}JZw&_PlwY6*-n>)ZeKOPupI=SlXHoH0gSy1LZ@LLA$3Eip4K zsk<(ts>Z!WjT%kd8_S)9J-_&mzw56Ae~ns=KH)F>IsM(Plp2+Pu-ZR9!QUhvJ0s|R z6h-6v%02WxJGg(7zfrROh4XNHF>#Au!1`CKmHGOAuJyl;Mwi`QbR8Ez+xpk5jnaJm zKl}Q7Vd19V`zY&QuU2QSe+8I*di@)9+x`bD&DZ~L{~I{{V2zD~c+9S0`B~rbqS!E^ z!5TXrPU4GIHk|Z&ol)?|1j?Km#_y3EuNjOH&fpiuMw3BoVGXKm^&D)D-o)cz81q6F zo5Vr9#vE@PMgzdDi@>{#CgUHj%HlmVv&H}inb(U30kbSW7y?8#yBLq5m12?OXTgj5 z#TZ+@2nKO9D7wx#M%>k#+21&Ny>1rhUC|8(Ym4~AWU)62hvOmv_>cMD1N1)%dVv!M zf0YS5ME|u!Ht^?VC|;V=|EHt>1HRBm_CQ5Q_nc_Q&N-0m($({&gzzt-e&CLRYhHra ze=+F~*=zBFb)!*#(sRkg$h-826m3q6-aLvL{P4XmQ^0!jnYUrYjS@j)e|p^5~9 zE4}>Y-kUeuM@Q|0&e7hF?cFuDczes@z#9d?kuopY-JS8}(|>qAh(^KRp#R@T|L`^= z|7)dMV^05{;*TWcTolgbKhXbpLfL(d`W0EKMOcfuZ*w$Y#yPdqP(K|vS< zSHZ{-g7~}_xlT_S7#nXk{axUsp}K0NIF*#5P% zy|upa5~yaxpQ=QwY6W48s&l8-rTzWIqqH$ z;;?=6vij5TkI}EYm7C&KP;YMhYx~Xn?(WZ9FKSm8f4qOQ^VaT-Uk(m;-}w#u{Alyl zFXg?q7r*mI-NCD`-^7zz`KtMH{2z_p`=Z-=H)vMQU;R3IHR!kByx%w&`*wFQdEfi_ zKQD@Qu)EXxeYD$azTCPv|2;bTO2bF$v7Ip_p4yw494)q4_MU2 zm;S^X8^*!{+nbEoelPIP1H;%Ju<=E}!oe7{>UfMryAiO1Q8+kf&VWV3U;v*!yAFE5 zD)1?gY&ePr{a`R&g~!Z^4KL|BrqV$2q{j#Fdq`ceaRg5e4z2De2w3oL2ml5HFF;M7AtSi~Cb!B)!MQW? zd%$0CAi$7TnH%{xMjXC}uYMRiZrFo1S6MK=7yu0T^*k8(!H9)Ev`_s3{LlkTM-1?- zvL3YG!}gMHSB(Jy@9@Upag0BhGaLp0E0$m*hyD$E;W;$}fX2bQ36#=%G9qkoOsy5v z>Ku;Y6^tY{jP^UH4>L#ua)S#H?9kBo!U4uECmmkGCJ$VHAS}a24oF1wz5= zP5fZZFrKrwF&0dg6o*XjW2_~)ITU;m;A@zQA*@YzLet=f-7d$Et1!48MzGScoHQ{% zF;snp6983uFiv3kLxK%pj>jWFMC=Cy2;xE9hte-2Z$dx;MqK0Q!Wf1N2-&6K1_?eu z;lqpQnlP~sICOCXqrb+j1G(JJc#P`}0|U9`_*agYprcXL>%lNqp|g0>!}%!! zB1Y#9kkJhd1M0=A)T%-;=xrGFaN>B=j(3TT?N8iZ=+WCzcm=k(Ml|wud_fb8lN2wh za^V22WkSGJ2GaQg2pVzd;wGj(kidGd`hZ8F2XfT$eI$)lGP0iiIJ>~Wsq^_)@Iy}=1V?20A95#LbwVJQ5-HNbWd z$GtHC3lukYE(4~6jD@;Rh{GO@0*>U|)d8b11PH_loxu$wEe&yDCRP^m#{(||f_fC5 zUl7MkhDYoTdl&_MkO9Ex{BVrS2fC+*612i^A^+kBxSW?Em4+vWd7S|r}J zoQ;^XJ$QD*{2=J3@nsc&LJk-Vh@>LmQ3K>=)IHEbA6OA*u!kN9HN!Z{ffZM?2Y3Wh z19W9XaKJ(-nc|^?i^X{*8*+l<4~^>!5|i-453fR>xO@z2fKmjBU!a+63Rwm)8O5mz zOyf{Ss+yx$N?4oVPmD876x3Z%xkw!#UkV6_V2IIZ z%sVHCB%cYiqo8elA$76@21|8x+o_5iy-E$j<1RgcEO=Aj^_7jORRxd;^#=ZHe6g zd&Z)I4c(BuluQW66~L^Ku!#}xVH_yZ1{@Ozc?_XbWDsEqmOpSnjDd_Da^#c)c#{kO zm5D=`{6l4Kvv0rEjPD=Lkz~;%|L!{|`i5NS(7-4HW%d92KTw!^)eC+8$jek{kHSl+ zb`Wkzn2}4zT zu&CZ!(fDuW|J&K#Z0{bnKYkw%*#G5HwULegu}hV?{r@TcHlyJU*9Vuq6;>|UWefhP zWcUs6Ri&gOfdfYifVG{2%?fnB%E-J19sn$4gx0fARk`)SA{QAM*GfQ%xhePg&iHi!tn6yTa=p^9g8zA8fvcpBpc>ceamyT{T{AAMIkBFZT}E2HW2_INIKPyR&h?_TL`t z?;Wg8cYF8c0rb*-)80KQK(FwOwSR&i?C|x*&JK2EY`g{N4-h`Kxwrr8!S<`y zN9^_9&Q=>9zGwqv8!vX+yek;g=FZ0Un^m^8@n+*yn`-SrD+dM^|4M!C z|Nlh(e=W?#NWPOAPmTi+78aT2p!9Pu5xP_Q&Lv0#5MY55ljBHi9A zS`Xsk0NP&VCP5rc!1MwcvCYkeMBOJBVsCN|LI*uL$0IiwT%8`DazhrZrYo=+V-huI z^iKABCSm112yBNt=V%)fDuS-k=N8sBuiS z(`O5;A#!qXKseY?zJKY5BW4Ymxwy<ag&o_#2Lo4bJ!D1jDL4XKugL{`wjGJ5WWW+sV?3hS8_+t{r;W#jQp^IMT zAU-|jQ3J?)$YGsK4={!s+-T(R6*x|9@a-92!?Ojp38Dp<-3uDwRxSR(NS zw_>h>UNjWGJ&qp^%dk!kaLA|WCK!VnKIHC3170R`xk^o=&x-rdZn(e68BK{f25!;9Rm)VTy^FhMsKjQVZNXZRTEJLq@Jwgz*k|KKbc z*9jv)X@zMTIXH$^w2(J_o9JFVpNt&ocnU;^fb!_llrBL}ltX*zViZlzFES>QwzbKw zV6AaY(OUYzeiFafjK2@;#9#g+FwTzN>~C!!oXwt%pV0}R z`9!&yj>H1%1`AO57_)J9@b<;8ofmJncecd87)5k81qP>a-o0cG!a0t3GA^*?Esm&b zh=yT505a+@iIhA`r;G*Y0(NOi!v;Rr)I23Tmqx2r3h=kHev4+C=eVIKpL+DNY=Kt4yNzz4!?Ro@MP-3d zGY*^)%mJe2lz0cIHY3nFExG;XlQy|>KNy>vXZM22_JyKC+j3Hu8~rUvy7_Wv958p{>llD4JT7zubR57`ZF+ zh(-CU;Cv8f$p*AS@n68HKx+eQ?ga+@WW7;HAZHqzmgX9FU0K!;YoD>@GiwMeyxVig zc0~|x42RKM$p#6|<>G(fXA%AZ4m^pAN25u%+Y9jFiWmh&WJx2LxGj?7``3+n|Oj2uS@2hT3F3KeTuEw|XWtcXIRIF3j(6?nyTo4gYB zjk@dOn*DYel{99>hdWX)I1k5#Q153Y)lnZqB8L+A0KIJ<7|z}Bom~(hgzw7Y>xTzn zP_8X7NYOVDPx|bqV5E*JC}Unl*P)1pQmA`l6m%R*^&6g-Yju1DD;xD>r?`Krxk50O z$8aXVZygHO?2+*;?fApj8)fb!K|nCL7)*yeYlCBfJko&^1Jh<0ats_BIB8%&C}67y zx#7l#Gw?1jbWUuJ%2rWH18V>~)l7DZTGDj`K)!(w0~1j|np0VXpUDv@V}D_rp*qAD|vf zoi2nPwBg2pOz~LvqhO4t09_*-p`xzI+3rY?OyTmX7iuOmrQ?+zVi+*RlO#n7 z*)`d+XQF~CB(YA0R^x;h*!I}qu+hFT0Kzw_6Azt^KPn(dY0l^xez(l^V--x1OnK8f(dX!I)n;1lb zKJ}dmvg~`_guGY!bbNymsn%?B16m0%YB}%$*MH5p=9D{53 zs>nEyq}u{_=t)mv$}gO@%P?jH0~^u51i%`{FPu2=fcj3CC3U&t2^owllE3^I1Vf38 zqG{#yak?0k7^ZfGgiP1uHy7v`Ay2WOLZn}w5tp~DOX%y2MEWWZU^m7Add0PI!3tri z6tmEytk^3^f_H68&k3`VEN zrv>8$fHmPhMkz*#olS5V3Bq1SL?>!HQ4e^Ky7~%DxpXd0HO zs9+5$-i6})(24w$lZ&UGP@hAAE@zk@3MTEs4H9Pow2n?30YMB+VtOMhco*_>T8EhI z=+8DqjcK}$@R_a2nYt`;Eh?EQRH&t*MoJ=Rkc$T|+Pgn#n;m(o^+LXTZCPml1f*yP zn~*B!3I}9Zlr9?iNB+39dnq1+;hFV=@%mRq!U8vnm3QF?*>20dwnPybbA`vO+d)(4&6{CWd_&yZ(MojHLkprt2(Qf%kRxU#L1WLSl-6%B)Sj{YO|x}#@iza>-P+=<*5jRN_|YIR zf_LQk&95T?pCo`AbP5b39mNC*W0^P!koGZ*!#3GEGc8Nn(Xb5P;PeeJbBcEniEy>_ zFXU6n6JkZl&lDR;9wXT&zqn&p{S>5>dO#c&9=r>@nPF%~H^foyplMHYkDG@s48Vk4KXLyA3g>Z7h0>VfagN@wXyl#U*?@86Y{m+&kE8i|^!8YX7o*c(lFyN<90q z-QE{pe`+7R*gKSkUvC_ualHB3Fu@*C0sIT|Y_b)~j6%B%xgdA-+V+9ohG^;1-SDG5foJ z@;s%8J=%bG)PS+Q+(GMjpVLB@S|DPS)oJC|30IPGF_lYEsVtwuj%>Y4s)uAOiwh=x zn-$l0foyT3346A(bUgB1FjR2X(RhO zB5yLfT2_Hx(6R02`2+@N~4W`>bZ@X%zU?frsF(8?$5=@oU z1d=iWc~X^lkhaBP*O;3vpGGm~dZgz%VI2V*}`o#1N5 z1?UVhPjzvQNAE`zLrsZk@B|||9f{EZs*!(BDw5F%my8rzp86141Za=69o%V$FN(ir9ZuNb@%ugwJxuOS4EsICN#v?I(#{)QeRjiMcz zp*yZTYUr{`VUsE{T9`Dbk5c-AfzW&>V%sLOY=Bko3K9qPegqckkB|tGeis&=7l91K z8Ho&sl;Wefc&oPn<*_dxCxKVI6Y4Yn#d-MQx{V{}Y$6X}M*JUhbHH2-d9EXh|ByDt zWPrJ>pbcz5+MGh>U zp2pOLI0Hs_A@LtwacWVo^>;&;ss56;_lHtPdyA5cq&*s0cV6Z4$>h3INHT z0tp%XDn!hbs3bWfEJ~7ZQpSL--9zw!oCcF2Bkn1ZjnXEoz6)!JQbM{)EJ-nMi5;Up zvqoKb&EJwd9gm!$>gjO%)%NZY5hw-yPj5E^`bHy;ute3u6@{L*!=sI(w}%CTf3BDG zP?1HXAGRp8Z?>qE>x`PEK*OmP4VjDIn}mH~JnT8>SL>thgGL|DMjx{Wp*GpgD!3q4 zXrmKjH44&6!5cN%`Z_bqI#t{v%J?B30*ny}belQ_TwaD=(paneiAiqG%l z`-D9u?WP%dj;VQ+IvAv!ZbF4;CubTUOE~K|WJ(R6wlGtDeqh|RrxM-JNcGIi9|pm! zBX=b9PR{bo)g@tyrW?aCZ>OETDkdOnRZQgBOpUW^(+6|`vuv`j)^U3A`@-Tf9nZ8Z zp?7HHxpJ4CUvixS!mphSP6p{2VvjuaDMr}SloEvFj#AVAss6+DeOQkqA)Wd<_df!0u|9D`7}SRr3JCcotR{6O*s&gy_1jR)>m$O@`a_~g10W1 z9YpRkJmksRJl;p1hV4QW(ZS_T;RICa7$!h8XLh=!Zca$WaI(1a%AJeV# z@x3b3PKvbu;w!FBNQHN%pF*+>Q2z8TmWX{Q>Q4L?+_%83Z9{Woh~@-K6a9YAj&F8P znQ7#DFEYN1tmlHz5qo*ErITn?)6+0ztz`6xoQ@x>?#y#6{X4~fsh^)J{?E3{|W~-~aeo^*^c5C2oc3c&zW(x7;w)W3p!LeCC(PL}X>-jJ9~zUcpB7aHAQKMN9SC`uc>9>_{|5;`WjFN$muC#MQ zFyEl7V}9VS&nlo(^g>tYN2!fYw};hkrhFjwS4ZbbaD*Sc<9iLH?ZX=q zd_Tw_3C(kI3vbs-YxW)2VzaJ_bUFK;ath94P~+s+=cjk5!BOS%otfN5nD{cN4+MDK z)}CBnpm&y!M>eoX#SjE-82nGT&}7sHY1J;MG?%GTvjG$pQ>8gji_6Qr->>bJ741DK z2+EENjht~M+s>)&deJ3}NcJbHize?3L(RI)y(N~l`10-Urr6!nCSK|Ir~s)VnsTTf zvJ>-U(LOQ1G4)6e==l!uj~h$|-q?Ut+v$aaOM!n{m=NlS_=c%3TDXujxg0m{2xn(X z>-VmG^n(tsee!(rhZt+dO#wr$xmZekLt_;y?U7FnpOW`fLwxLMUAz-YIQhtU)64ph zpb0E(Y94wq?MQsrc;bWl0a$4)o-+)`bo+@UP~muc7Mpx^upQ=r`o3ZP3v0v<@~ZpF zW5|>l7mt5~8c?xhwF=g0u~R(%hQ-BB=Q)1moku<38LBC+1EW$c5I{A45*$B^`_E$d z?2B&9#jxaN6E7G+O3p7LmIj4`fq2w_Y$9NAcpv}0h!qm{d4<-)krcRaO^V)sj0R-p z9kay+aypa}*Tr%azmssSm?RPe7Q7$<6i9)h~-`AJf*^C7}BWFh1QWrA(T#aam51woZoJfcOe*P|TYN z>y~U8gPbhqqPy&Edq7uA(IqG7>&D~abhIuS(VbyjR~)l+qeddVasRRd*Z^u~2OW_& z0Km@PD=KJagtHocrNYbux^5%JLwD!=0OsPYtf9{;4WP8BQ~MPaLwO=#4X1{b!X|txn7y5d5bVA~ceo&ZUVES*Lcg-CIcsB9 z-+4=#NK02(3O@#n)e5LML|ANkO(A8>Iv*ol>0Co2Jx;iTNx$O_C(sUvq}0IRP2yP5 z$(g(>oS=bKJb!VxCHjXNI9gyg-k2u);<_+^#%GLHMG+oO278C;HqL=XMT0Ke-9OmF zOC|RX4m*3hJG&1x^*r6oEt3s(rds;&A*ERHNMp-mzlq>8Ab}!&Ey4(p`o00w&^PQ% zt)pA`SSa0@naIe5b;A2K3w2G3RZe9!kYX?N#C*+Z@-#RS&fOD)X6XDqE^bh;BOL|6jR8M5)49ki`3p_iXXG+Dmx2DK#~uWK)1;2Q%EH zMifoP@Iq6Hd2Sr`&s?F-OA|}x#X$2Z*Xqo2SImqarYU(;=^5kZOrkK0+&eS665#PN zq>_rHX4Vx-k963BY$}(Rlj}^Dke^wNG#MFM$yCN<{&|cCmMAMwno~b8Sg_AnX0$4| z&|{`nlzhyzCi6N=Aj`Gy`?dR*9tz9=4&PXR{E5{|MkC8X6Hl2osP>pS6U9ExgaK-I zWb&Af$O@LW@#RZ_lW7T+s($5+)=iA>+I-#K{PFPZo6h0e{r$azBVd5pX6`h7jl$Nx z$~N`2xJoY*+9On0qKs@6WUR|H1>`yWfM$QDUo&N}a!cg<6=900`N@Nas~y#Zk*85Yg;i^dNBq%|)t}^e4zydKA5yHcF)q|8C zeVP)TiN1QfFa_J4&|FI^ssgBL01h`(lKewcy;jOAIY^`ts>yY}diB0?$S6d?=Uhp> zosOZK4Ajl0DxngL^?ZD>#&QP*=Hc!1)0K4mmTI%amIsk)d1V!b#+Kh~)mA8@TD{7a zF-MgJ3@z1yIJ3otV?u(OrYqMK3diIGqPPrg5KiwLY`yUXUU`Tstg@}=ta2Vj=Mq9@ z?GnSSn0>3`;L0?jpBc*eJJoL12a>o|ryxp0D#M&G5Tj4E|-nfI^cf20n_6Xf zG4YTOCb15Ln_pl6Ms2OBfY}qu?0s^!3f~Zh6P|$aMC2VmInz_rsbqgLs6$PgSo;pD zavfe(%zzlAW%;8UYcv_aw?Sl~-A;i@3=hl6wCH!-mkj8I%a^nYaHShnY7?SG88M)L ziLr6aglC0c=+ES zw}eReMB&tn5%JWWf5h86UFcS&XuoeprfUn}VL~i)5ygjjjio`rH5ZNGS8BwHhj?8$ zR8=|J?}KBiiNRI6sN(C9V-@9)-S~A2bKZoAy{WTMY z#skRsvH}Ki-;eq3eRmpFH03t!fe4k zevctH7=lS0bmYk=NvGN*C3tMkHOyIjyA7<$-3@VUJmv|dyG&kM*^lZ(ns&fyCxY@A` zF#FQIZA?GQZ3~T$3~S28FD_tpD=fvnri`GO*jdiS&I(N`Cll*CmTsI*84yMit41C1 z5!_P_S3|r%iL$e1U-6ujMZ`DqQ|3-CROKAg*O;NtrSN%5IDQmd=57Mw;<=MC1H%Nr zdkP{ZPJq10!%@)9px0(E8gR8hR7$h~&4Z4I8uN|jKebTF zUeb)hQX@-DeUKP(fXgyC3+fACO(l9027Q}Ux|^1uw;~cvFQn~m<3M?Odzri<>5G|P z^I3vf#C&V=Yf@hmtvT{Ixd@dXmcow_UUwk+fVRw3cbTDN+Ezh}$!h1$YB6`PY7l@JZHa5E zD>$=C!yt53y0NB%E{qO8oRGCpq;FL?@iFa#1V)(wW@>V9*H3QtnP^ zYd;d>MZ=wGl}RR3;gOv()^`e165s^ptdJ|MvvF{+@vEtYw2T}~yeL~e{^2wUvMFnw z_9g+v#@v2IRfOl7f@8!fGzHi)#ckhG9SrR_fnw$qYIV?+(Fvl5Ja_4sNRA0VZyfAy z@4i}NsYz#aH%mNb;TUfL@$qB?&|<)E`o%NJ*OYBUY%LXZZ^8B$FCqcvc}|&IydcDz zHrxrCNAK|H2Vll1q$`409C(uvUd_%kEX0MWz>~_1(Q3Va)KL!tXT&cZN)0?aLb{4e z4Q}RmR&grS&>uBANzom8{P5W10S$TW+>E_&brTv?ltuLr>i4pZ&f^-ferJozshGwU zQ!_yi#b)BK8*g?pm%**FoEzd+8OX81)JTC4l=?QCLZ3aeWJVQUT{`i3f$ALobmfQ@5rUNuMN&@9@7#pDVR^XNgW4r+EK6Xsd*n9nL@cB@GB{2 zjDz0g`G92^ita<~$!e77*|~`N0fs+y#PO7XpREU5X%qx*3>0>G4v&OeizDi?A|Kwl zj`uQ3{`RXh@H1#bWz14T5Ilzt=RJtUZN!tT+{N!T?*Ci?^VSU zEU*_)1hfvHZAN`ic|$ksp^&FU5~7yM-8y3|0^<8*J@4?*V5N)U#fwPjYG&kv2j&{p z_{S(v(mSxdYhtp&$*?eTA8I;Kk-fnvcl-!rXYBRHmkEW4mi9@9>X%Bb!I_H#741PegiI?u6| z-*(pF`QbxS6P@^ir%JOds5_ST)mXf!#M1sHo+rX=96C2HLPM11poqnacA-gM#QGG9 zN%lg^!ii1SHt*?@so`bV{6h=uzIzPn(0&>4##ccf-InrvE|D|O%zv?d1-f=LD)NU< z@eHat<1J-8sxz7py@E`{fsIqnEQM9tFUw&{n4%FHt3%e2N+xqJW@S%cAJf$1Az#Zv zys$)Au!UQOIbi{*=x?#Fcnd8oJYU!RVQCog_;KdR4|E1ubO~rh-C;wj-8;m#r?x0= zdCO~6yvp5hd}Jp$V|hoX=^M^IOgVruLQ>(_EgqUo(}i?ZC`Bf@3a9j2oVY+MF%-OdP8P)4>%7l;tjLK z=jukjoZ?74-I!5#8CE1SuA~c2jfBd<(+5Z7ocDsOToK_#!yC>*t^Q{QUE~X1xOi zfpwzR!0Yy~JU+zyaLJQO+0=u?`I|w+2I2X|xOc;+OS?)p>*q~^vZrSKca21IXMOgZ zi@E$=$2#w#ANlO-ck;K?FsM+c=YZ;YK?kjQz8TP$L9SiLv`ZKpoYnBM@kQW**%<4` z4b!d2b(skuvK#^q7@V>beispK3X*J?NEQG+KtzNjp4o;~Te%nvAetXvYo;UX;D z^4Zc!X^EpjCOZ)IomCky6@wKPT<4ORHjCE^&({`CzDfgef=!$hss5TNJ2X@{jd*~K zWa3n&DUsiMe?MmA&OG*)og7pKMT^BDuySQ9VFokcgyy{fbk0YP)4P&qQs@MFhs#5{ z04f)_4(XI>Fho0MKH$k~T0XAL>(QHNQ4PkyU>p};NBux^3@m=HoHV$I=M#l$gnOxi zN&%&zp&v+GeMH~a7nh~DLN#=WCqp`LH%Xwct#MI*Z1Ulor%NkbrGJBh9r{hO9RrS1 z^7dtMkFj>!Nd#jWG4#kxFeH0L3oF_Gd8^2WfcT@plFw!@xoqH8)mcbOS5sVhP?(wy zy`Lvw3J9GLxZ!1RFQU#cQv{oMki!)UMb(dFY}%_tXy%ny-=-_!KRn)RniuNKEu-)7 z*f?C2=Y`;1Ainu{M=^bKoiTkQR)DGo9wE%qjc8vW55M)8E zqHADGr!y4LO)ZchN73Cll1?gZNyZVa0$2G8;z4dhPhbL5(B+=vO&N!%2d~>x_q{w& zHcy#O+niE~*W*`$zA%mW+dFPCOZ1~M@FP6W_wTrsc(W;*N^W z?;tNypo(k|gM#wzi)o9(VX{Pn5O;S8ItXiI+?4+i$-$7xi|{ z9aJHno626*Q{iA9ByMOtyK%nG51yfIq60a|3K+4M! zBxn}BZ`zzkwbbNj@_C3v@0V7x0%ruA>Y9tZ5dLE)SKP4iP z;1G4LvH!po1&J?OyHnV+3cPrXS0s#h8I@k#iPvN!c;YE%1^7!nNOgfCzQJ1QH$Q6Qj4=g-)bLiZ0{UqvoNhuj?h4hwh+!7eu-by z?S@|H5L9wbMtn|w!OP;Z%{eg!J4n2$7Rm*>%PGk*ij@71@IQ9I>Nh82!Ageag%^tzHGax)lAa%XD~A}ZMkT5w8!vfv!mB-wtukq zVyFGa)YE=a+#Tf;nRVV(M@4xd_-nBXzZBEbO4=l}8w0hP*}5ooM0nL2)88sktIQ-Y zB$dIG{!lHnW~LL(f;5{LxDyTD$3ZS!@`UYdz;bS)z6=I~V0^8X5mavj2`=%yUa0-Zg|aSHby#Cdt8JEA0EV}fMFZ(wCNMP%O& zMp+z1Q7_K6ilT+MjKl!C(=ewZn<`1MRoV6wXG@V)W*b0el*>5~kc3TenWH--@&YYH z`8OMzdxyVtw%YqUd%t4-*3Qw!!K?PsIwnL^N^2tKvO*C59}9(LjedYWvy7h|an<(9 zv_6v@aKRRMTnH(fsi%74pOk>bZ@0>(z*pS2hP`I@pi(W8EarC{IhX3r=WJ?PU60TQ zGBtF%x}0u8*B{3`Z84t^dJ8jxc4Y26Oy7;pN0nTQPI+H5<4TR2GRYQ^26bbKCIhpV zs98BpFhdPkeB@kou9C~FJ&RM5t0jKPSBh-&eg$X>byFq)&%{KPEGsvqsHRURy(nGq z-kzYmID)5X@1(PnOwN@8)0Dm!O&vlD7TFOD|D(^ z9rjfj3wf`{wfd>nu{J5ZKTUter`Z9lnCTSVJdK?UfF}sZSjwz9~`dM9O zPbWp!uwLZqOcDnq!t+u5Ar)q#6U)7#f|mwia7N>$%LCF`n2E=kzhQ&snUWG2Vw72m z(kb>-kgB;tt|g6z%;!^=Jf;bhoPxBcm3gCBJKxMo6Veoa6M*H=Ymx(BKR-P!&M-YK zV0zrSar!b6@*!R;V@I%x3RmIB{J%=a<6;TyV(MXfy+c==lTS~u=;8q(=TjbEcgn3Q z?nqLm|C-^2OnnihdTqIR@za zRBV+tYTLFBFn$4V@ED1iBd5xGGU(At>bB2YQR3ON!t*=sf3xijtaS567t@TZ zXmCwJkk3F|_MvR?#36%$;+TjSw+mJE@7OFOn6H5bqoaVXoEMV?ZGuLVP!d2XvA0lU6?m2-C>KD1aytIx9NJeE?!I5>2?oFvVcADfEE$VUY(&niT__cZ z^8l$QW!G&!f=!`iIGEfTs0M`#x7Oa{H~$8-8ayr9yTRKlE(1SjWhEBqiz}X`mK6}q zq-HJAEWl$yp^EH#@iqZ)?Q8xLlbu(>Y84x5K zNyXiB=+SismtA0aH>;G9o*Fx+6R|@)8|w#1yMWwS2Xo?Mn|y$)ERJwtv1JU18R6x^ zxZ3y31$Kcg+;et^AC4h7M1ClS*B^4C=6sDJX=#&$T?RMag%i>_k-`uj@S9)gkTbtN zR6CIx^eKcWHT6Yo7i-c%Z+Z3?Nyx%Cc;htQ8*e!nl2c5)WmMGN_dZOgf(VE-2A$G9 zgd(AIcS(2mAS$So(j6krfQo?RfJk>shteHG3^Q~7~me$-shap z*$aiQ*d)4GpEP(MfAE~ptIGG3^Bsi9Ba2>#lLvdoB6ScWPK!f3nH4R36-5TBtYgX&#S|7RcSXEfC$)aB@QA z`1a|KZfo-J9?L&XHx%#|BYqgSq9Qw54u0;h=dRmz$28+X$m**mb@kR^G5y9FK@U(+=We-Ty+=Y2#Ye+5_(Qc-ke98~A;+ z(l9PyHl}WHQ5E7}P+DvrtSFj)e(;qubS|Yc2dZ9|kyw1I=+klT53dLFw9c%qEfW4# z%^I7?Ovt^1F-p7f_cS#9e~Pm{eb790dQCNfudeEM&S$u9BC%)ktkE~vlzz5=wVmKW zGTBSft-J^4am<{3my#hIPWQVf0;BiRw3?Ns@#ue;brd)XJ|cZ8(pr^DVXH zhLruzqk0ro@Z1SEr#~fm`-dZ8ny+t5Ct+KY_}NDl*_u>nUJC2bGq?S}q9v*?)g1Eu zn9)hzKUF7)&(RrNE;HewR zSLfRWJr#P=5>umXdRv@%nR41J{haO`cH^2JU51}o!HId?^?2@$dSSi&L%DYhJID%D z+7q~{n^gn%W>gj2sT7aLC@I&~JK2NbTI5UVu(HUS@+Z?fX=hZ>mtr+@p3*fJOCL4UwA33Kdm?!M4{~jPEIXWKZoo6y@M?zrkim5u4lmmGUV2~78rr2jU_+Uj z;xAAi{C9)ZZChgCTZW#wZHR$AsVS|@OG@Ke+Y^-+U9~^;(SzTi9>LgS{btUAeXDsf8xj9+Sd^0}pW{Ql*a^Ge5+khXWRZ<5VI>%x4D z_#*K0F%q5ZO=Cq5*W#Cb$=Euc?!0>T6y7ee{oz|mAWOdA_xbiITw}i!w+>nCA$snx zWF}J0Ej7!c1u_`jPI6)1MEF%|Cbo3>GoI~{l9lxxX4=OHT$+|6O7|NnTs+}&jXX>( z%JgrtwmLR=MrIhzYkuCh`bavL5Rj%II?;Y@{XL%fnM@H9Nki?GB*pUO)qPC|#_VeS zu)M-IpXKlO=%NabS}s>Rb$8-rgCZRY#ZH7oQD;|cw*fx~JNlNiv3Y;>tKPG*RbN<5 zZxwBpjr*E<5Z+2!WL34{c5S^WV;Rw< zG;Jbz>8L=*YdxzVOdjp_rYFuFxk4yz6^2XpE4@M*&Z@c9w_>wLD}SC`r@#c$Y(CC4 zo4tk&q~kJ#e!_VsL0OVSyTbolbI;?Q$K*7fmYt|o`|D#HpHaF2CUZMs1$Kg_#k|g4#juVG*v9g|% zJRWnLzA>5F{Z;#wo?l`%_go#`sQP4?)%-4E`C3(OFksVhH6>Uso%iL#qQfm~3qr#a zG9!Il2LB?-5B>yXF9s;e`N}oFeOCJHHp`QGGsei^5yj)LZiP*J`dVaDrEDo2ioRMP8y zNeiJ0)GCHQRD2;yNTb#8a`RL;S93jXfFALHp^>wtN42^v$a*w*K9SV4L5 zYOxZR?~~h|GQwt_eYfYf*`KYOHwIpfGX{}&$*@p~6usP=W;G3%Et(>6#3Q}?%zRbH zy*=5#R`P)IxpKzl*=sjmE@TzgoUwJbaNHYsA^s!3MiyprMRp1aNrC&?<;iv;-W`$k zGJUTQDYGgXHiZxQ_yn@L*?vZCg+_UZUfj}WYj$ON&PC|i)Q8&L97A3UlRp2`PZAlTVmv9w}X0OcVfdJ?O)tfab{EP29#xkH3{(QA0(_qBaR({f*1Dw z#s&KO)Cm20~wE* z89ZzXj4I8!Y(DIAY;Z@i9!{67jpBw+B7<+4q+Eo^wJBU&w^ta!yzLG8$1@Cx)x8kW z)><*&%^KQ6iG{qBDbq7c8_n0lEhpOKq*RX4IgQ3ZDg$nbM-7e5`0&0Yk0lHJqS_i>NNO#dYj14iaOTR zS;XcCsmoDZ45>P*TuA8`%S&0Vo6g%p-d`GgbqtiNPrbTMKfY+Sur6j_fqaLj z{~7rlZK@jimmdb?jcrR5vQ?)s)`l$g^JY$zf6`<`;1TD$wHOgFyX)wg$DGCcSz&*V zy-=N=zw%2^EaH%?b!$P?{`Jog%_WA9e0HafJ8nVye-zKn@Atx?&qXk|J>{s5VXqJA zmAp%@b_wT7DmDTi^Jg`R|>t2Bwl&E7A2hWUcS=h2d~?6=9Nd^oOLUb zx`hUBYkdklZ)ljNgJW-$iT%bj{{DzPiSMImuyQ|SS?$reUb*RtePQyD?O_$&HH_^z zpd{I}Lpt`NH?BnDQdn$Kd>O{~TJ__HTWl%ku8!AlY;FyP)#?$LeW#?j@w|4LQ#^qK zbtk{--izLxEBbki<`*YjwWpcWWv;Qv>mJl$4VqoQY8{H|4NqCA(WxBcM7@A$)K!G^ z!XQz!{3u;So^4MvEVNpQP z4(*rSU-NDfU<)=`WzGoPCECAF9Ia^bGfhu(nr4D7XUk&_{Xu>$lExB?vf69YSfFnQjoN`tihXqPR1Oz0@&;2lLz zSK@My@)-oNSZ&A>`o*rAI0Yp>JdJ$^=;Q@Du|&QF@ye!S=`Sx=? z9T%s4KJw@XEBW*eP3ZmMhs!6Q$jL`$b5$pr{r-J#zxQn|l}FusGrF(ut$ZY0@9uC4w>uqrN#@F^l2Ih_>j7`BViFr0vyle# z=(8=klUu9-uvF7_p3dwno%faHYg3OkZ zG-gOv?keOZl<%1QBFO&jom=Ol z`YcQddVgzFqNK_?Q@Hn$r|Ft-N5(tC2HR;!`{?i1`l}?5aCBP zvCV&&vFyzjBVP2HBu+5t&klG0nLJP?T%P5ic~s;rkBetK@(tP5=5XHMf8+CJ1nbM= z_x}C8UYQET)*MnIx`ASR6_)Q~MW4Q(d}|gG{&9Ee)-m5vWR%HkmJ`LDyR1Jj3L<&0 zrD9L+S6yX$-q^)cH@&k%q_59S8hs}mcRjCkxg?OaoS@4Ab#V$v0`80Z$>(CMJR-lQ zj{!!+i8%neXV8~af3Ln0)D+HFjj=8T+AetgO%I91EBo<_Cm6T*hPv~v-`g8!<5VP+ z(Z2?FpP=P;UZm`&1|)hn5`5Y73TbI{3WR)OFeu= zSYM*~=MUAC+DPF&=6&~l*rP3CEt@murb`9Dk$xHrsR)ce2Zz?yBp(*K` z1U&pUlWpy5yk=M~UX4i+wY>6B580t?@HtFR`6d%$U<=ciQ?1b4EV-_AvrQ@lZ4&1=LhVo6_cW92#1Z(oy<`K*T6S; zh7WHti+oE~67sbTI3_55)o?LtL)O?W8)nNr;j@HtGO9Lqcj&f4*UN@~vkoslLKty! zp1;e?i=jT{P3FwrJWr29HqvSxe6dKjW8_rx^D9Xd`@BI*46Adzp}-v{v;9C!D6v6m z^TGFk!N+p#dEVLa<6YEC-Kbvj-yXXfBeS)avn#U*ALqIjQ_JA)Xwry1`gDIE9) zJ-1UI9UKj6;+6i^6~5BvbM~}8cU~U)p#Sl6K#bMsjz*dbi57vaFe_gIisyMgNvn(*dZ5NyN2JXepZc+Hf{``RgA~32dbpuH?g->-|GbwGVB~ zx+)GEM>l;wezh$+Y$09^Hpb1AY$AI^c7xng=ZLazDn6f*>PfhVdiUVXtt3(FTBE{F zoGP)TgP#?PzZcwCSpp*5z})Q8xD`{q1m)b*xzHU)t-s1owS~?ftmy=`2Zsc~WtHMg zmT*?0lD3BJ!(P4%=lUd^`-@AK_131!bhoGOR$LKt8~$okNY78r)#TR zsm^%}zb*n@V;d@7MBINS#@?jWOk&fRGj_9)%-zApWN~j~xbvb#jY6lS)q3SoEWA*7 zwP51H^&)`mu<>10qGcZ0dKxqGy)+ufVN62roJE*tk8P71S@6AwYd$&ukgpUh`Lry} z-ACDRyn8jY_DSYg;FY)c$NYz%`T7?c-{IylspBpVJ8U{#KMxI0?vtc*OqDyGttCI- zGdkaUD;Mf{va1)ewqdX&9_Y0b>9bi<81EYn_CMl&fn|5Tnh+<=siiW4yhANqW#R}YD84HzJ0}?u`oj{h4;RnxIKVe_-&Hg@ zI`FUlueZ;1ewm{zyZtg^pAs`d+*v=}{rUIh{$jo~#GQ#r_pfNT$_|HJVMcRT+1yHV z4D+0vXk9Yj8Uiz)n>(y{4Oy$)j)!XZt^po%sF*a51}OC5(f7`&)>KXlsX#Ai*SDD0w$!)A46~bk}Wa7LtI` zC1+2j`ePi+&{LZ-+f3Wno4i)w6TBsPI;(3#YJq$163h;J5u6Z6WuM>?`SWQ3uC~(=>o?xGxcxv+Un0)=+lJoPlS$f-L(bio*LIQ*XhYu44t)k6uMhODpdEOF<5y{GL(JfWg6^O$<1YHA$Z>n-}2rG zYbMeg=W*;6V!G^T{&~6hTU5vrn!6wCk#6;=R6I!H*ISk*A@}zZ8N3Pxo|N@!T_!3Y z41*{%2TyoZNkm#HgqrX@BX1e)>e#Oc6P-@(`FM9}lS&s{Kcg3mTmPe87hozWf2>7T zYE`2ePVF*4@%YpI%-si56_){ScKw!+t#Q(K{)11D!=|GqW_VS1Bkm@`dqiku#VX30 zlv?{`uICO_|p zR@rmwn<>rBZd!`EG+{etYHp^w<-J2UraC=|{IBYT-uYQ(JSjzxq7B>N@(m)(VzX1z zo6S4Y#no1@U=J}aF~-y+<{tsYx)(eF54f(@lgwNvnVQXcr_`rZxSi6nUzZQHAK|$PfK4QDLtAOBXMMwCVm*t>6X|Xz{kqQy?N26uSNLI> zEG*UFcC8`-@8*I&Q#?}*-PWzXw~fa1R9%&AVgqp>exB7Z+`j)vXFAQ=O?_oBqeb?C z5^w)2tFL{{VU=&z{ItIBaovSAQ&Vsp8kS|XiDulTur$_O?uk|Lxwrg@T)~Jdf9h;q zN$Po2O8i=Ac37M3=bHUsnOi0gk4_G4gnYVbLKQCLtBQ&27|QML2~%7yFJzwG#dzi> zG7(SKz~jM8KRw2CW+K0LnjlD%guV8%1Zl<>S0_n{6aU)a>rtwD+d5vcxm+ zkI#CuM~Ur~pXy(oydZ!Zga|zJxrB`UULNhC9tQLAb0`#WG=c7SY?^_PM#Ib+uxSZx z9_5>?Ds(r(AhC6fr)IC9C2Meo)L>K(rzv#RQtqOK{`!#gj#KqiXeV2N$|F64rUW{L zx|($L#k4=^3<#Hr$`0F^-2&~D9hh$U`c#W)o$fb{p>O*#FM2#@{g(wpHESntG)44@ zCx=oeHDf=hfnshAzlv~Mt&_PavQAk_N6>`Qc}6D~H?l0KpY^=Kz$YrCQD=MFwa&DB zb&kDeTiRcO#PsC^7x7K_+zAh^F@LcS%!+q#nuOYiE+jLHm`vd19~P;6No(lv<(~Lru;%}m0}GHIP@LRb;G)yDMo&4 z(ui`~=5-$&BI`w0+)^)%=OYKxsHV-(R6}2ELphiM+E4)idL|&&9eUfW6+Fm5-uj8N z#U%W=g7LF{1(2{`7ulby`@qXptaQA>fYD+Zm%Q+AYDJw#xsLPnUeaQPk?9h4XDEf9 z=?9nD+wd$ou*fyDFNKEcoLTWgQcK(8ML ze^h!sq5_=SQZWw5kqNMQ%xZNTO?^i;o>beQi!|$b>GmXV8};}8fXZF{EQ_A<(A3*^ z2@9wtSxCZ0s{MB(YRx$ zA(OY^nL&7Hj_SZh10KS93;cdcCT*0N<`nM?kN+3K{wbF_69Fl)<$&BIJpnPM=C$A- z_}ZRLzE~HXEnnU^gFwJ_@fh^a_Sv%b*=e7$8ZNqrQWUKXmc;$uqQy!^Z1K(>f@Co!McR|@dVl)Tb#r8}a zbt}abalIZ@HEa_oHcY!qJ%{Uhhz%Gt=o8Lb`82&K)%96WnX37{{(gpU_H-+ML>UY< zqVgx8tyg|TqtEU~n#0FkbdjH3%FP>^pR;{ZsatSMWxPpA140OI;5F;`hm}sjQq&{p zpS@1f;}6X{3_d-Dz5sPtiay9%H6e3erA-YBNt<@8;*Fe~IgShU#BA)Uilf z&eUUr$Sf>%;AW|bCjl)b*<3{Hw9}*zo6r~Do3gL|RrP)jhDN+F4bn3JE`;;1%{5yr$Fze#KpJwGN zM{M6_R?$?g=PDM~%n;o9s|cw@Bds|R_-BTDigbgJBvOI2B&+vm6KIc zbx9)20Gyp!@k0|X;n=#WFUI~q;i@5&AAL7ZW1gbnEoVT2xku{Ts|&3>Y*U*024Eu}77- zAIvAb84%++_+dmyaAOlx`T~FzlBv=OsKF^PGL0`WyReu)Wk|VdHpAwFQ|J33jb?{) zH{m81JasB{^JUjtR^+A;eXb!MmF7qdEa{O@26Q?^12YngI!u28)=%{M;5mOml{kX} z`;@K-mjp_;mCDZ*;Q=!|XSAr<`;K?HDy2BvkgGy3`EpZ&cc^`TIx5MEdv8NkXmAdQ0Vd$fAj}RSc{bMD4*tQXsXw9H{3H6b1Il) z)gMUpX;@2%`nH+sMt9>?BMPf;fPM|SaRq6UM0(2QVv;ZOL4Iq4YtpXj_}O_#1R=~_ z@VoE>`qg}@<`VMREhRc(wUX3odtuYpF;6KSXW5AD0}reA6Vso5e0-;(9e3y9hf?jg zk0o?cGfTZRr-Lm66OR(d2TbR(OZ=Z<>yP*C>LVf@IKFxz8B;R9$F14%Nkl%$wx?jv zXt{#R0U6+V3O|VIL@f`1X4eC{Xt}Y$$~#_&>s()w3TFaTlG*LI);Lbj3CpO161p>N z5G3n5;y_{(a=&K&6n+ilL@m<>CXxtsl2JUrP#)!EgY>KDQ>3PPLdAY;=8Fw zdStfC{MFH}sOa@)?0&4j8~dnuZsIM<|Hi&9vm_ zsqnsY>0klbUE?`RHV#Q2TaC1;#J>EY!Tq@b&QT(67zq%z0N@hX@?>h3o%ubV^oX^S zr#05UCtd3mSh?}l&wa3xz7@GdQKcJ3YX!t%mP5f|<8e*T6PNY>kMl9^gG4`w%l4HGS$gm<^~eAvORy1LR~diHBhK1z_z~ z)r`VD=ONr-`E0iDpCt3SjpL=)+m4i<1>Cfo8v?TqJ;Qt1K;&{64VM5xEf~7P8K9)D zO{YgSPU%5kcp>GVRGrse&g&pyCvXo}PncrRzh0fdy^-|4&g_8aA%IBy>uj+S?Eh8G z5-QT%ucf>BM{~((>iTxtJ%?m21WrEZ^as3BMT+AWx;;aH6m?p!Uq>iBOm+)(7 zE5S8X76qq6XY-~Ks88-@i0fb9<)R`YfIjec;VTNDIqs;2os-((adP=;FO30iRQ@=y z!#Xt%bZ$dJ`Y_HfRDN59iCwVps7t>$LUfDc2Mz`%D}F9XrLljKbc0)ZCxtunmjvKE z0(0QJ705xAFzbpa%F6xI4PbH%<}JMR2_j@rnXeog7Nxc9_+6s1Lu0*V z^=y2Op1)nDY4ASI6pT&=GOxg{Vi-^1Xt3D}G4BCg)OSgvm?Gsl%}nko6p2d<-GQ~7 zMa~EvrNtu5=R}JdyYY1T#h9X7DZN}V$K5DT0HjV5XO5w>#BRLX4a2tPMgx$~HtH;s z#@p(3cSN4Qh`hnPO7%I9FhfP>orLDHe`Lc(zQ-UK2G9i5gg?sa92Xx=NFjF6m}qFD zf8)KnG7am4OpSsKep%=VJ5{c$Q#7r|T%p@Flp3=uvAw(Nxqfk6(@!}AbeFE+o2Q^= zUl0GcSFt-(vJTuTcMBa7?&|e;Z|A^p}~N86E{AA!@##xY`JKvZjbo6 zuyDWw9P?n%7nAuq^I|DW?Q6dtH{Ixz&(UrTJm0F-hZO>Ch(NKvpIn3?S9kQ`t2Wb+6<10;afdOqdJNcuT}l9b-qPuLliH{?}Em6%kZYT7l;*z!J(l}KM!gM7$9 zWJ)g^CgCkj!L$R_Y5KQ~WV2k7%+S6yLZ@Fg$7shA9M*Pdw3ZCu)TCsc0v!jS?g}al z=y!Ao-zp2fP`7wA^m<_-$xbNW35UY?^u1Z-x02hzBbiT6wFy}S;s%rPB#F^`Es^L; zT~^itpb!*=@dTc`AQ=Jim?}cD##mzaQlt3VV${*{h_ugo~m=2 zW}cLm8oPlPw;8zjL%0p8Sg-^aC^!sg%|P{~DuDM^D_=X`U!|SM)=K9!^I1Jz@pOVd zuUSHgtSUvu%eBIPfA6!t z3-2nDF*pM!#{Ane?v-el!ul7t>c`YjB7=DIi7)^y1Ag5|$N>sqyPt+iw_&s{rhyzn zJu8Msm^4K!Pl%H{+4y6N^4-@{C1Hd5CBAy}ra_b6kf~?04Vc3o@^PRg3$7M-ff;DP zVAJzsFCvmfMqLY{>~-0n%YnL?7Ltk8?DwOdIoOcv5*qXGs+Ec}ZafJu)6e#wY{kG? z1=YbRDH02Y1`O*c2r~zV2DqClwp4h(Rij}ubI%p!l8>C2Ka$uy%MDK@6VFb2|Hi#{ zHdOd43|t@koB_QH=uqYFYem30nF|^bpbrWDa@2XwN5(vNiLtRj_X%kZrxaciIM)VI zRI>42FzV6S!8kHo8)4mqIgjoW*gDTs?Dc4jL4TBE^yW^X^%&Mk=nqXGxTw(1H6w)A z&hb1rVBbL4pizUon3Qa)%cv#mhkns)a+PI?os-_YcY8z_1_|cmk5Q#R*jY!QgY#e+ z3G~s4W{85bo{#B!P!I9usx%L~$-$}39YPx(Cp*#gPt{C+%_NZ(w=6MlgPAyagRy%tzN`WFVxzB@O?`zdPv6h- z*Oz;PZD64J401XQW|2S?9aDt1T8RL3i4zu5K>q!k$Z4vmi=C3uwd4+O;9&~7bDFpzi(8UXy?60kPx=#bxvu{4YnG9c;l zbbsAG@>xWuBf4l)vD;m=R;`tj;G3f4JErS)5^^%Sd2wrqWfCUcFBH9uUdArKDDaWQ z!&1?r;LSbyn>)Yg*9QO4N&62<`LYDpi=D+QZ9O34n$l+&lvSDOp_L&-Zu# z`J37rhnMuI}Nm&LAEIN(j*w$#u?azw}zCV zFNTFJ!`VbsI>Ki*s`Lag@ zh0UVvPJthwOuU9(AfbHO;91w;V>|2fIcqxZ|tv z!x)CNmvBv8rrzEfw+=`cWqXm`{tfF!`A{_c2}oZA;k)4JDENnE7)J1pqp_~*GcrS! zPbDUi^KPRP`t`4!3xp(|MNR1RI{YGJ9urR8M;!JHj6)@rvBHSOLg_AqX&Pt%y7F6& zYjqoW)H*M1{7Y~@@HI@`edglww(z3W-UBv9Q}ztsA(q$uAC|hdAQ5sX&wN9m_rr)1 zk*`pXSzbF<&4@ttS)oyTvD4DfnX4IR}G$U5V&|DCZ)Ww zr|%v2Fw5_?t;0^Pp&cz(ap28hU3;~kx=%C3hIHB625mes<9p4^5$84HEBSLH%ZM2W zh7O8YY@Lx8EJAD^LK_{w9Q0WQ8k_L8eykl8&8zbx0f}`VsH<-W(Mg6Smn?1`Vc z#y0D(BV~16?uy&*y-yP~0joFA%g10Eiwg3#P9!BAIIO4rwPp}6unS9Gu#UdY zS2re|7;Z#lzWHgH0>j2kEV1x7xBlf`NbqJ0-fn|V*O&`IDDq_)6}n_GyM%oZqpnNJ z=AajnR2#1=nz#3AV1VP*E2%lc-ZWr5(a;9W?muI!>S7NySuDZ7Nx&V-9_zUrm~yr+ zK64t!Q{tvq9q~qb=edTx;TCsUWIVPu`uO$V3|P0JtJG)E8sLcrN@s8;&Et1ibExF; zY%cW+vhbSYkLp%Kyz$RNO`Sz)L-&nK64l?{x~C}2Sz{*})%LH2W&}L%3#RrT!L8n7 zX8!C#5Z^GJ?gm3~Qic+EA3xP;bss<6s74FY!{ z9i&%~P=M7+x=(;0<`lX%8jI1Z)n`0CL3fxFou|DOrYIQJpQgs=Cyfs0qN2G`F>OF2 zNjH0yZw7!W*d@h)1MmX@G{!)P`M-T)U1j@b@NemmG6by|T@mu6OrtD5y1Z zB#qq_*2ohWr@ni{CC|@U`J`^%@?VU7inMb2Hw_rbR|f;DsC>yY;VKsjYQZEG^~hG| zn(yyB+&1W0hGoXXSJwdGlU2DR-jT8tVVIfPfSM|1V+a+Jdvp01b4F zmOvNra9@V*zI`K`X@~iDDX$E?N2*2CGJm7l#LHedOTE-)J@RNHB>l2KXFuwQlOIEC+*ieX+w@9Nj2urlCqQx-X=DJP0vMHsE-Y>R_OQw{akSWg1``9*T0*bFe1jwKGbG00bcEn<;6m-lf zZYMqvOvTADq_|5?%^i9?aC3(gn^(@ zhrh1IP@Z#`WhoT&j=+TDVa5`51!evf^C38g;UC6d@H$%)iWFsiyZ3AAOaEU@^#iF0 zQhA%XcIR+$o+@KsCIhOqB4|v{g)X5dB(DNAM(m(GH+rt2^I#qcUTi^+{3r2#6{aKe z4@ibe#oaBK#As_}Un`%<)tNf-a{HnjT+9m+J4`Fr5fn$m;q#F&a5xFQ{Cl;0I12rJ z#fnAw4Rj3U2#6>HUV=b|wns61{h}_%*SSlBs4scFp!QAZ9f54WXG0&9W2 zVr*W71e$0{FRfANf24^W1(((dJ5VO13hvZgc7I&g%G+du=ZX}57sPkt8-!T}9w=kwVl@B#f{671`%iPeF-;zyC!_u{ABp3LE zV!UgB8wP?0)+Isu^v{j{zH`YyVHW9pV-SXdTzPu0CKw9+oDT}(SX4C3l4v!mWOiIofY!qu6 zi$`*y@-IOvfL{R;)TuZ2M8J?m+-;hHDx&5Ka_0ukOMi>S_b@76gX}^*J$8ctzMW$0 z#J9Mu5eKMUQq&=8WC%Q6u}VCJs=*#B^RL`xi8ybU73lT3@9As*SfoTsiuj<;fq!jd zig%aERwTwg>;RR23!5#%hd|E?oM{ujpaGi}clyCiYeqf!n`~gKwn2U&^WIAuD}OQe z2HCYIEIfC{**-jx_rySgLBk~)vInA@cCm+df-RdP^I^=i6)|$l-=%BX@=330EE-84 z;AfRi>f$l$)t+(SlNku#fWL9A(6jLQBOkzjNr$+!M#nJjVx2WS2692DD+XbziVdAX;bm$vIB`z|CS+OgHRl@+UK{7C3p z(IvzaU;`XoA<){ox>7(MUZBj;AA6j<>2rG*9eUena0hwq9vK8?;)It}7-WZR=oZX8 z(i=Ho_ngAgqsRDHBUyc97n2_SfCpafEmf!680MU2fYiSya12AMxfX>*AS55&CWUqG*oIQDNE8z zmr<78s9_L?Hp!1lCqZjzVUjO`_>=XEbIS~kG$vdnUE-qkS7#RcUqX5FX1@eI^snt1GzxwBtuVBwGC|>~4G)_t&gC zzw@LQqWBk~Lycrt$vzqm1E2&`s#OitPC7B*Q*aG7KErv75yl=*+8!E-vg`~8P}yIg^`(n#wGg>j=?$@;?9|@BKF^5`8-@0FWuIq0wGx@S%dV5}t*n)LPcRB9 z{SG>Jx-0=^jdKW_9DiGbyU)|+&L_8}W)~CsFkA_5UJ!so{ zp%6-iIyFHZA`}0O_VgY`(87vZn*tu;pf@;O>jI;iEw#*WpmgV=B8_|88D=WLzwm0Z z#P)usTpjZMWqR%9@--wI>|UlTqpV8gogEeS5*Es~_ms=6`aXf}^!rd@6f2=5vq1(s>>fa0}_R0^2d$^fvt5Crw_gV~z2pGC% z|NPyEd~mN=2&G_d=!1@3f#-CAJn#kU8{qR6Xv{dSi0W?Y!Wt^3yT9D$AC|3FlavLD z{c)*=dQPcJ*+zq6Q$Lx%Zeb1cy095b{XfHyw;;s1T>M^Y`$*Hr$kZvn1-u3c4S%v{8gdlnq=WqtjrJSq)+pt3B}+|nmTr@ z9|S)ASRy5|*{mN~t?+@2!&hmmVs1P~pOoixUoTH+h1Z>k2=th zw&tQgC;NwvM;Dh8Yjs9{h*_7>v*6@(^MedJ;-4xeevd=HLVBu)VsFBr#c0T=DVC5& zKk8JJ`|OQ(jvmU9_j9#wNZ%7MYW2D#r}6mdWX-H#YFy7#wfkdo!}p9tJ_GvW5%^~g zK3>M`Uc&7Oj7s|nIfw5U>GhvEvqwd0M~>aQCD>(@>aJEBBJy)?ib0Wp^10(qK0C;7 z!9Yl5t3ixAHUQ0>fY0N|hEcec>EZYZ->cu*y8E3xK_hIq4NV_?B-F}dA6AJ7*p?+& za+Kl~k|2T)@!Jvs<0SMDnLh+dSHRut3zZ7VSamY9$K!Q3V?HmAX}HkpwcOc|Fa1T| zxRK@%giFOJ`QcOdbF}a@7_IHve_wZvJ_+) z10Rl4mE<{ijNg>GYlhpEgs!atZ%%ihf#~H8AdN|UgZ8SyaJF2|K1^lEYmtfBr7VtK zW#!tdqES?w{##tDI}`+?EWPLh6$Vj?*E8GaSSa72&Dxx?WGSd z&9Y%S6$JDSvJ!E$yj*x}eq)-(<>jTBs@-?>tO!8q6gWU)G3<}p#S$C=XV;9( zVz@B zszEsR<_kOXf9y>?2MXxJ@TV)@XhFpqL%BgcG9$PQ&BjMxwO)m?^2C01`xnNE^G#o( zDC>FZGKWPW%}9uB4JPmq>utzw;KdBm<5u3DQjqd;fK%`UR)?-}9(nWEt6-EmyvUSjLq7gLuD&~v z%J==>h!hzKC7dE;WoJ~5Q51>HP)7C&NyvCmRzhTpqm0auS<*2hTUJp{_R2iQIh^y{ zzel}4pYP}W``_ih&wXFxb-k|Zx}S+x2OW=>bgQW54~o4F75U!T4jK`Y+1~wQG7J0H zggRN3Bd`6LN=B7dF?ppVI#@Q+tHC_#C_7&Ew65R55^f5o)`&y1QNNJihpC5a_G6S% z3&@4-oz9!4lEuPfb(9DESa0T9d9@_^5U4qOg4n$}Zs(@=aQ0et>zap=5e2rN18)CS z;A^9tyA3?al72(y296h2@=|@b^`$kK5{|+z(x<482z~EA{YJrDodlVZdwY;dl|>tt zXA8+B$x#JzuCusM97_GVK;ZIXad?PBFI4x-csxKRXp06>45vrdiXe`2yNL?f?u5 ztj}REP2dq&CDoj_GPSoosh>Z~GhD78Ji0sh)xDNYvrkLh*Q*~Bl=Ssflj(iT2wq3$ z3wYxuu5pL(X3!6%?v?Y-6wZxI2s3Fb+maG~`_Y z2v%$!5xn|M?1JC2l|b%r1hva@625Zco9FtuP^G72^m72=SbSarOrmV}Aiav6Gfj}y ztc}T_v0fR!t>|eRg>ja3orBlj`&L)ehTSCp-hdW9k^oYtA%d%Z4o`bGUn-wGw)#DyA>i3}sgh-=LUQrU<@;+1oHAj(@!-WQa0Q6zivgW9u*1H) zGY49)dDLz=4oeOkryBn0=V8RLZgfn&;b*G-QnURzew)*_LA4TVd6;z)`hzwZcL70x za}qawrs`Wt!MFGa&Vg9iGXkUUPE{d23>ugfx^gV@#WPAD&87| z4~o=moH*NnV7`jq;4K(Kv=fxuVmqBIgn8Zu9CpJxY{W^57$zjxt-89GaBVP;C=8Q3 zLht-6Ub|YfD;}L}1=dC38uGqc|0C#>=HB()@TT!SouMIe(S;VTuAGh{e6r)i>rT%j zay)Nqr=4)Qi&*>s0@?NxE-%u+ACG>FhvaU=A3b1U3)4iE>pY+3K7KM>nj%;8oUEkW zYb(9s62>Reh(jle!bsc6evBkCgB_$~eCr9t{L73-FOu?-FZ$8M6<+-`OwnZ)m->#y zuY~n_0MfvFqcQ52Mb9L(`afn{QqV)Uqv1p1(+N^=G0k|^E--HT_1XKR{(?x`-yQ*{ zq|fC)JbMWd1Oi*wl=DagiS0%V*8;*GIC)es@44u8EQP}@pZV^}9)jkit;;3tdn|Me zTjrW}lV5Ca+~!>~`3213PLpkD1WBGtr?6c6M!%F|q-xQ9E2nM2PS`6bODbq6p~`nP z+Gcn%*VDA3(do8ptoY=@F7I#XQY17^MDLq@X*i)2sed$f%+Z$#T@1cFYJ6T<$-G44gkB0a|lsB5L*fz`i8+UeoO{tgP?P2 z7y3qWgJXvQK6n=jM7+(N2po{C4Mr=1 zwr^N>i~!SB>6xmfk*5+#KaN(2+z4V3ybbLR;MtdYyJ&r1&q6i*3P;4?AL8YE5ZVh64 zC!t>O4Y4vb>QC?xEZ*{CtWauE7<$<)bgomTTw6#l_)&r4`@&YQ#3$Jaw@*_>a~uV@ z960&_P%3N1-~*?DULUhOT8R!Q-4#YK2c)Dh>!g$ z?g}#H;MExmop(r?0rA`X`x!?k3C$r}a7llg@n+sE$fNL!znJ$=mf zJxrIH;{NXAvn}j>c0>+A{oG*YkBQDWvaxxdl;;~}nhh_zQSV7*C-U{X$XhFn^tq1I zEH<%gtQ3>i#e<;t+Auoj(mvq#ASS<=2Aws@+2_cRRu#MVUHWYCAX~lOdzHI;3n}^% zn8DNNM!0dr*B?-IwCQ9U*Y2`(0OK z)!uJr6m4Sau6;LpJO!b3D)uAMlox5L{#lU0Ntw=)$7iQG1%C=|Jw23GoY3%z6ZrxQ zgN%7kpV~1SSP@rz7Z%+&FpIE1C$dh2^vF1~d0Z)2njVTzj=#^p&YARaD{KNz?znhJ z$ZzX1E6tJk2g_=Sr>-yujT2Ak0U3CL0Y(bjL~JG`-vwHiR^^W>mMEZ3@cXP@>eFFa z8OSLde3VQujYbMM>61+$0J{pQa8kcq_fhET+v>(!R1hIkLgk2OF$qi1zRkEjC-yc>M zu^Mj+Qq%~SowxTH^13#$KF1uo&}tTW>r^aZ*oL33o&nY=1i|}RyaPFwCm;0R0r-2c znZ43RlR=+c?F zifO=($9~;M!H_?UF#vc3BP~JiJffu87>eJ>8f1%l#MkRw{e1iWZ6?dvvTYHXv**HC zjhlEd6bwGYOF^h6^vgzsoJ2e}o++G&^DZRFKEE>E(2rtZDrEYmK0_=&+r2V+Z| z)f0;6Rg~HXYuT;RThY$@3G4U2c_7+uZ?$B5!D;JeTK-bn8jae^G@qgy2fFk%UAf|! zS1pg5)IWRjUN?G2H3a!uo%erXJCGcM%J-VY?)r7f2RZ1TpjC1%a?RQ9@y1 zNke_AN|ryk_NxB&`geRRE@x~S*!t*XYWqn6k9Sm<)&D}QzZ76U4F!99dWLni_JY;}Mt%oLdX5bmub zqWQr=ul>BPv=Zp_0N=5FWc)&E+iEg&N%zDAOMB%_=a=3cO1JKC)fy~_kwW7%evIa_ z4Sf;KhVJbFw}8VK86dci&oR%S+8BsHe!aNH8*lP0YcAoz;E&ofrP`MoDPDfpy4{tM z@B66thw|ykuolljdlP0tAnaBKm_F?6<6{gE{0-2`V22E9k6B!T1(V9=iw@oDRhE{O zYF~^{N@MP+y-~&J=p#qj+0mZRa&r!EevnW@?5&9eH+k9+atAR6Q5b0L5jh~TRU=qk zB_BnaA2x92V56R_?>}km)Zj^1jb+06 zYH1*${3o_8&~Ir3RJD7nT7az~$d}D+cQ@Qi$^sCFET9)j=zy)+=km>969Z2`j->!5 z7kPbfv*@%~c{@|&vuE*SxKr?sj&tcUW-??{2Tf)^^D|@lLg~zk)2H3VktOu{6KIdd zf1U`(VlxZLgyz^%EP_N0;YA(LTOapk3F%L0T~5Y++6F!dgnv`lZ0`4QZL!Fo=nH#R zSU?rtXku*Oy{o^MwmWx9VbV}9;N*mpkv~3Xj@}#BiNSd7=!2hu7q0+7Tv;L=vG`6i z8vt9hv@?;hJ}zW*E7BODg?%H2o|0P@U&fLNg=F&Fg;3&_YbO)w&b2^zdnF5JzJSB- zZiSOpSfo7i;){^4c1Io#d3prnqNJ|n>?1WNWu(R5okS@xn;;Ua7pz}^)z;reL4?H> zi*FLRXTZ6xfoBqGgXT8CLomoRjb?ll1#n{pZ(H?MQ(_V;*c+Z^3ribLH(ik*k*Peh zHmJd#^1^w`DgR6)pKfI}5giA8xP&OgoC~}_2R4JC-3;i}cTbU*0>e~OZ>=BQ+6$|& zbgD4>!uMi^w&0i(f58*8=K~JfEuFLAI!xT&@62_9kwIxv5cJ6l9pA2(k1AwUU$M80tC;C&vAyde*da~m*WuDaYCjZ6`P#gNV;F-mJ;0330QHsv zH?6_BW+yFsM7Kvw&N;Qxqg0?MS5=R!fGF`eV@=L(gc z>2K?W87@(2e>o5AHy}f90t-ONfj4+yM6(qx(jd2Xm3ll!xAXd-*;HZTJAKMTTBS#V zug+_09)>J6wJVIp4*c?cf9THM=7R4Ym|L^B=Z2)6bKgnHe4EI|ljWA6Z>+se(pyG; zR2)m%zV6?=!-wr1^SmP|!_qvCmq-eG7%Z4r_6Z;Wqcnrc(}4{2LKl+}xbVjR6!7RX zBQ51-sPY_nrOJ0_ibZ;6P+u>dh047|@hIEt!H?nW6oM_O(hAqaCyobA{=EV!q)g4k zgvUxxRqy|d?hA`=(6{NvxyV;p#tVzgzc$=1i!Xi}H(7#w<83jg)=~8YMRAw8Wln-h zT_n?D7hfnfOK?b4y^PNVc)|`TJpdO%co88SJqfyB2gz#XK6$Kr&^r2TA~sl$P{@Fb62+W0Fwrs3mb4)STJ@Lz4jI;VE|Y{F{Q!sYb;yq zKhhUr7D~$PV9gjyoOmav=PyCkb zh{0gi%%>fsEg2nP3Zu55s~_<|XAbnyDhJk5KgYRHQk**WFx)ZvB-^hOdN^YR_n71# zN10yMhwd;Tlgxu}9yp$D4#g)04G?;3W-T7Vs|(1Nktuk5Dco)coxbXsX5i48;pf>y z$D7zZJEG$2FkvFyRsA|3ms3a1u#v%WyPn2}2e^A8X%P;QJPhNT+tJuVdyN}~+o+%M zqGEK-nKJ#qrMe834sb*hPE9uys=R$#@znbAk#5z)=fRuxSh+bQLC5rCNVnv25cPtlEF^OIf{KfIoKX;S5<7RB7U&VOXNCow!5 zkR#OV?eA!qIxQl@hy+ub$0a-1z4Tw5u1fctUbI z9*dm#971G^GZB-u^nKPHbKWvz?xdTSO&YgKLI z2Mi)f5dP2{)~YF-q&B_{{9*JdnqTSOLJd=*6z|Va9kWAug}kC12`jh}&q$kn^s1fs zXoc0BWZK}%y1TBg=oj@mrZblP7=gPr!o5JW`%Uu`lECdD<+oc!d2cf>s!|l>bW4|5 zJB!Iy^5kA(lmI#Nzk%H<4g zNh>@t=^gfTP)<*?cg|3w(xeG8k^Km5#ii*kQk%&-)e+kwN&CMY4{ppMv z*iVrpZpS2}0&kOaI4Q47G2X;zQD#g+EPCMgW&XGs-LO&n)P^gxNO8}EWRVEh%mM#Q z5=s;J9Va6CMNN=O=TG|jIiG6cyM2OF%EeDYj%inJmxTo>PicPkFr(m&>EIl13PU`- zoB2L7-8+ZoE(dF6@WJsxpj$LFbpP!!&Y+fIv0vMY8i$Ob&rx4b8xIVcX-rtXzahe7 z6%;Pua!5WFn5}|bWc$6KoQ(DQVSU7>0&uwRoW9olMb2hL;vz$kc9zf${a~Yx)$6W3 zR$=ecrJRi_%vslY4m_jYP0HhYC(7cHh_$K3luVdS{+z(p!xL<-z#Ivd-cj*R^Aouo65!0 z-=BXe^zCtGWEOl&5PmXi%PumVT zIzJWq+ycL0e1PI@P*79I+ zD{9AC+2Thd1<_4#_8Uo!8&Y}Aeb-tOURd=uCmfBWQ}iCLUV{Ga_yX8@03)0Mvg2Rc zWQ!A%R^OWER%f3V`T6S(j;R0o$%&0U7yaI}#%JkNX-QJN<|1odV-L6x3&!TaDj7RK zL?`SA5-_0+W25U-GS8b@T=^t3oFeYR*ZZmR>Fpxih%qn% zV==(p3C6Y&1i@4V9@Xyd*sbem6}B9W3(T?z3;y7KpY_)GrTJ->Cf4Y-K}q%b5Y@y8 zN?OI0X`DXs%@`iLjJzF!aspfKTdUoZ7@ZS5*GoSII?jHjOVrXlo7>~gz{$_y^z-QV ztRFT#zg=e|=s&x>djWc;F>amcALJ?`IvJ=fV|m`f^(LgEaSdV1iXMq2L+#dPu#@m% zIQUDyvv`_J&37%zO4(h%Zpzs&hwF$tsie+jB8~_lVIrSA(maHfi-Jtjo|jv`9XJ?Y z%7|s(zErv}?h+qS<;4_>(%=qbc02DGmf7W4LzaI5)$b1}uDZZmbb!eijEllUIowq1 zN3%}uuBjAoIECM#9}8k4@I5do_#;*QqnOPCzJT~ips$U>AJ$ta#@jCHDYPa9KsG{bVHY# zK=TU9Ee%R}n=tOg_DU}^l1qZA>6-Ahk7~)ZYMNr*9=C>A#G7uhsCLj)lhILNSKAa) zIt@}#-Jqv-3d#3jvIV?Lb_Oe$Fik~)CS!#YtNaLmo_|=s*46B}hk<&hQhj5J4e1wQY=HKF>+DwQ*4NoY6yIza} z13&6?wQR<-wBO!J%dg1{FzAV2+~ibx%zzB_#IC8BZX*p%bD)abm1@HrUWfzYWq;~A z9`v^(A<`4?ev}R_99yA+xtLGw8nNZ5^8+RCGYuDTi+Z69&eUa zU+FZXf5ZcBfYyK~oZyL{B&;n-@O5?n$fTvA!z@C~8*uYeQBB{IIZ3l(R5WrokNuO1 z^bJ-K-ru(yW4obGwodx(z;Dk)#RU9f}8z)8vG$|tH zG`nfEU$C5traj7NWp?Co6K;nmiKpFVk61H_xz~%34(lHYZ>&iJeDFdOs5HqsUS)}; zvU0lWWhegh73HXuUm*iuXFN^9(V9??&kK&zQr>EPX6wC(-mQX5;xO?zAVol*)+{bj zJ)F;WPfRPW4zWEfB&d0n5^7-7AF+8JFw}IYB;4caU z(H-Cm(r7fhxNZ0&eLC)gF&F=W%{oi(srwt8IU{{fuIX3kTtPki^qD%mqRfx#$8%5` z4whNhnM_d2;EgN5a03f42gneN26(Uwk#N{UD)T`BpgWX=v8ciX!pMjOkvN8}#6asK zv5gR(HBdk3$l6>ZID|AU!*jmtnAq=t)DGj!fZXu0IP5zEVX2HpPT_&Iu!(sN&Imr| z5OrHq@#wUn2o_#~cVp$r5F|Y-q6*sCVDjw+kG4rD28xX-x>GoMO5qRJ>@&rV#GJw{ ziJrh4I5z9ml1h|!?kRPq6d9N4Z-rzZc~E-0`oNDTz~320rgQx4NGLCg@d?Two|Ko_ z%-A`Lwjjnl${+f-h-psq!3iwhYL0X-WSb!Bsja-8b*hS$>QglnsCzx&kL+KZ;ywXA zV{}Lq^pa_I{LU*p&u=E^>23H*n8yySQmXw?kn)ycW9G>bZmJ()&_@MiOb?{4#~viM zwUKaIl!eZxo1fPVEsgH9V7_Yy1vv@}daicZ`l{1{1JEBo#w?;^E|S$Wke+TO8=9iq?C zj2149Yd9kxZ^X>bcB-I`yZz0S1@hCk0p|&{~2rEhW+#y3P^_>DfIV58w2hk ze4^7(VbYI9{-!NjazjpCu+Ksb9lADs(MYcPNii`=n?@d*mpRmfU)l8zi$++Ym&^QB-;f)=3 zP_C08J6DQuYX^=~p3rZS93ic$>r2h>W>Z`}OO<$=%|1pV%NIwj;`VvJ5+t4hDSk#@ zngr(L(j&dO=SnRewU#YZomI5GKEV67?F5U`K$c0gRLunI=d%Y4l|OVMWlA<=01m8> zaC5C_)D#A_4wb?jPX}6NE4*J9e*XN4GnhKkX(1xg^KLWU4u8%huwQjeJ5v`YTW_*c zn)S#|(pV~fjY*A<3tbYUcHet1m5|tR9 zG>&JNRE_UAMAS7O$W9&>xE9>*&3)d|v8TD^olDuPx?Y^;IxZ7~)SDp-Qcdg>HlP9W zvw@3#j$|sc1Zfx>|=k!G|AAq=03?Y9cQ)F1y}qp4*V=p+4^E_nMacj>?i-b z?pWKp8*0rhb{0tY_gXyER4y3^aO!Uq8Q7!pbYD%S_|}z=Q@6PxF{}67sh%mye@O`} zr4rhlcYtsm7`e0tQsH+LzuIIenwdC*CZ=dDpB;IiUfNm0#UEtDS@t72tS9YPwhABX zVgy$)#n2bu#I{{~wvFt)lEd_uA)H;RXJjW~1cuK0wTMK>`NB8Niw z#XSYyz`iF~q%1_(05F959aQ>RXB$iK^T#i`;XZr2L1hO62XG(B;FEW(oZIjYj)Y;V zF^9<8OGD)4(C5r5wSD7aFoc_)!aPhRjEY7@ruW;L5#w98oWoKG8jhzZ&qN{ z2IgkK2QskS0e+MuX!-z!zdZHY4Qgcx=UiE34*pUf(v*^=`qr!d#X^}hw$kis#t%=< zbgI$2B^_#-g%t?lbF^*h_c$ek;E{81hl(x!+o(-L}KZiI>H zm@qkZESU3*^Ia>gV^d|i(rHfR&kiqqCn94cJD<@3XTauZ5W7ub&t8!is-7KO z)g;V(TkK+jF&~#wQj+!E=jxA7dsZpfvAphhELhEM``Z?4KY<)I`jrd3iUGTEq!Hw} z<`{m7672cxUrOu^ju$^ny>Wgvj(&clYsXPGl2^7iMZq>Yi%pArMgx}nVmxD!c?n)c z9$ET~^*;kP{wzJh^NqdCw%wxgHJ&%mNlE=e&tAbmUM`~ym#h|LP43G&GFHk3wqqmcki3gsXpGg zQ+7DrCyCb3?|T%~`x$*v4vYbBZ&GvHIumLQ;3u&~LR6Ni(Hxbw{QVE_dbn9rozKu( zlV@`eO_-M{OwOFP_m}UXA15K}ZV{*c+42IG$sX`%!zI~ouueA_R-+3WZs%3DXe_9U z;UAOU=?hMDwB5EGK4frrG9~KUdC=R8y?6oC0bKn$h#P=VJ@BP8l5=izhZD+Fj=P@z zirj<8ve!mJ@~f;1j(QEBe>8D7Q&pK&OtbID{=&;dA`Zr0K-?M^Sf++ivJ@}t6*d>G za+UM8vrE4;`@MSVz;(qq$jrxx{8Qw~%j&ul5uf%68^inEwb*qd96 zTn2%OP%*M^1lrH}`coheW@C4I(d4htG`Zo@ZhEP`|GJ@nx*I5UgSWcJ2-^qOpjxd@ z{-n8cWjxM`_-2^9x) zT)}%HWJLlbdK?MqGy%;c$jfkm7Dg=dP|24}P^S%p`D9oIW^yB}{(Z*?UH3t&f&-ViqoP~W!G$S|TLUzP3}tTg{uS(d&pOYQ zokJ({_FT_Ehwj<9m(EpG&MiR>LQEMU-9}Yi$76D`5GN1=`2->pRRDRO1BamP^j)dg z8ooHDj=@i?f>Sc}&1?6%7muc<&dZ^1Y>9gEr0+QlhA35t{(475g68@nLUzs~`rRf* zY{SU`(DN#@sKQV-gDvPCyTtI<;HHk-UWztbOh5CuuTdR(`mo9=t@oH{u`?d>CZQ1! zjx>SH?H^on41DurQjFKyra+i;b@Qm-7v75nWq`JA5BSLScYe=Ulo^sN-VoZXs@&3+NZ^K)dq*dUmVD~v{J0L#)7$xzf71m zyTQ-(*j_Zku>-g6{o7m@gr%3ni6RW~hh&fCIag^09QoZO^G?b^_oUpgLdRfA+)hv% zXLhotH6Ai1K_TlvZiA@-wSq7u5LyJF2>{v+1;WJD@334MbQ!jY1rHB|=zL0sqM^o( zK$74%!j<8X32*@9a3u0r?|js*)xjWRC~9^5BXQPBiy{zQJpr%BLjLPhQA~Ra3B1WU<;rwU^771c#9Yj8C18oDgLdpCFnd*E910-TJ3 zw(T(xytVtgl-zCcdC3^ae%kBI9#P^_9`Cn+cD8ohcvfaKFko)ZqD`(JcR6r+fX zvoGm?8N;kRhiVTYv-U`TiV|=?3f81Sc(+1f(SsGk9FCzY-TpHX#oq1xVi-j|`o-%R zLHrWz`ZXPj#Xm0rOeQ7C31FCZz~H+nAVz3|p7#1tZr*fU8%XB~Sm!f9*HG_ntfEOl z;<&wF^oz^k;>x=X$GKhFAI$JEM?9kpk2|~iBJ#(R)3}sJjrX4XzS~mb_+IiFG)O|B zJ+@cn)?$|f;M8s8iKA&0ng#Mt4kVp`d(Ibq>B#z$ODkSVQ9;kNEaFsK`YBH%@O!tq za<|N*c$+Zpm~1yfX&d@y2&e};Jj6FT)0puYOyDfUoV|I@t|`TKqcZ%3GG4*@xs(ik zUedUE^x4H#fyQ808S~n*y-|`yEcA&6ITOG`oI_-BGZ<(V+s&)*Ky@Q(t*YUS$egB; zW7pHxS0$1zoA>B4tE7F{`?VqwowBxlTg z!1jGE!c@t;WcYYtLbaVot5b-~*`>##yBZRg#f*eQ+!@YR60tmBnVsYYRxfSfl;Xhl zIk0RGQ$P2;8u`7L>mYp7?fDQiG;2_2pAx=6O+Tq?&0m=1!e2SDneEs&3rtB%M+mq% z@RJ|xMv|85NK3sW9gR*KY|!OAf|!0lnyq%RUUYy*Y8&$1UTInJR+m=IwPp^ZTW^uJ zk99X>*E^tA172n&)_4Z#qW=iT0{Cm`01zCuGR7u@Jw-RO-a7-wgvVDy*e@tzJC zb>;IcpMus+-wa?M(?zv{dvLKIlyr;pj4ow zw9M9^kmJs<;^a%ZRtYt9^mTFt{AQ-*-GNcl1N2p*r;rYcAwnrqyiW&vQ`o?}q|(*T zU{@YkG-R0dU#DWP937D`KdSeXiprpGG3ld5AZGN`*>Bl$VqCu^ks*UAAb>E$U?mPB zT!ts^z~bvzTTw%`BGTM{jvQIh``Y7Bwnb5wI?ltd6 zUVTc!*?`_{@RJX0Mv?~VkT;mXEsKl%QlB(B8{X>0hAN$4Z+>)h^Ob6$*L}B75ix-i z(eHNcgY!F)J`-fmdr&o4{f}jUcRI;LsQBM*j zu?xTH9uKc|16d-r6~-)sI<0>U0}(y=>YA)AKIYts@3BTGp5;b{GqxY}?mVeyTgzWc1R7lPOBCC>;VmdR?QXw)yha z@0IoJ{a1DYO|r5@dFqt67#-ONG+dv)!_CG2v1YtPfMG4{X= z@Q@xZ_)SE5OBYBro&;=y*gJ~GpBlV4d&=C9i|lsacFujf?hztD$n`Z685j1}y+_F& zaKq;i>GA7XfcT^UKoakotKM%zR08PnPQfn?vFK$%S)WKxW^4~Oa09n~a~mlVQ?BFgH6!sG zNltsP8O_*Bqt!X&WE7C>2@<+m$&zJ@k)xru?#MJxIg{*UVT)M1|wJ@g3T2a zHW=ukUn0dI1jpOJI9H2}eiHhg**m*Q(b3&_Woaq-CX^t8Ss#pCO||YE z$f;6zv4h5a&?9)k6X%h-GK7Z|fjc8ec8$6mPPTL?Gc`6zJ@6z%Y%Z^f#%bYVmTg~h zW_B!>|Kn=ADiO+L$WvJAPQb!Kw~X~|#H`4h8g z_Iq!|OSELvVCLY$3a+vXpAF1eoUYz;W}J33f0TFND6h?{n~$ADFTFhH-Mb$WFCl$u z$Z`+Zk>O?jA#SIp%(F~=&9GpPzPp`!@4KwtPpW&WCj-h$yFlaVi_)Q6eckgb#5u%4 zjvzfw#=kJVli=%zGw;ux;;Fh^;Kj*0B6&176p`z;q1ltf^R-O z^1%RBRE24F`gxL^ZV1 z9=mG1?F)JQ=6b!8VWQjrw^4C)6@wgQho66WPZ}!iIajQ}c^drILUygVVHWe! znMpl|%8A$FmavXn6GBG0!o71AI*86u_Bpqje;M{LkL1aK{8Ug@VS2G{FLx_B`D66E z(o=Qq?^}s3s(1j>m^J~)6Vm~nSo4BVqfrMn-zx98r4Z)iRi=@v`mtm4%O!XD;o*Q^ zHjAphZoM;{UX1qwcSz92258#?qeMjK9>6-LFt!*D51pxZb;zwWJ+X8dFPW2cQ++uB z!~MjFqgg1Uy)ewNt#4j;hGr8qjctO_b;J)T0l_NJsI`My6|sg3HSFDT;=_h0XimaNFAkcONd7D#gkgFi*lO+KVc*!5J?zAirc~zBF!-+0olQ&vKfA< z`jtCu%@v<5%KXoJa-HMX(J(je!gk|GlI*jALOf+L=u`4X;tk^&jW`I=2yb zts#N`mKt7Y;Hak9ZZTy}vDyk2h>y2`Ix>U+)io#{Fd>QFlEi7xy_G2W7kh_~=6W5k z>b;?{gOe3`f;!q)Jg@kH!YNqc>4*uUSNb6H8u}heK{1 zYkzb~9TQs2P5#}dN7r$dla6?~wnu3iCPO}m!1-Casidt9c* z_}pgWWe+Y&(_0B4TC-Gr%mO+tm!ryLhpwMo!GJLW^pC;{5~1~*I~e&7)kTxkolv4gM_ZTGn zj>2Q0neln^QpwnD)4B4kH#GX}6pa#z>of##54n)Pnuq=ZqBub518%MKgPO_aP9su+ z+k8{rUW}WE6JshNPF>rj0_x!lR;w%?0LJK;!+Y{zwFmK&NLR$yeJ>DeB+FIiH1zKN z^`Wt}afvVY?hVaO+ev8KO+16|2x=8Mn1~gJ@tFsK@a8ZxKX9asKRK3wf{k>U6F6mTVB$B=MgKqHk&>X3T}OW)GZu_;HuV`q#`2C{n6^j8(mYk)gUUx>56qv3am)Wz*AE0px^BeO z&#^^g1S8~XpmyyYTkT?(#!fu$byvBKqREJo5PfJi->Dx6m$433wvapsvF2&OAMgOS zKOHUqH6S)WyKD4YhRLpr^@L3H_|rCogm({iSV$U~yh;gikdc!A;HGohca<-bK}G^VF;?L8@aKm6U=FPR zOZWWOfn7syY=nVo?Uwm#Mg`HJi;8x4opWR~XznSB8rBC=L{=3yI&$u*o#QdurEZqAqmEC9s zQRZf0L?e~hw}GO!ufF4ScgvUg5ph8+QS@kW=~Y*X2SSFaCa-$;5V-!|3pHU4NEp^8DNG%48M!yHyXlXdfJSCor39Yb#MqrRPi&MR$Of?cLiTYIRYLhgg#xO;D8yjFDe8f-p!ir+J2ff)ZxNcuO;+P<&+KaUqU4Y=t~u-v`= zKuHneo4G}$SYmD?m2fvMR`2tMf^5Oa;33(Um7mG0VD5j71bTl-5xil^F0kimkOo5+ zibZ$cTr2s>`M^9VfZ%blk1KR3Z77uL@{-m(X#SV#%|rnXWvvGb30B-;I?`>5?LN_Z z^~zR3$nDkF$QkxEFcgTm__@?Q`tq(CF966H~0dpBdIp zG#ezo-hHj-@GPeJyVJ=^%jqbEGUSnynsG$mr;x$1Kl)x{PQohIonLMIVbne!_;T^* zGghB@t-}g-vlcPjl(xQNo;`<;zYSx{#s(3f|MNcAVBDj1(36sge0`|=N!{gwvNU~0 zFZZG2o`VzN+*w>2p>*2ErPZ@#H+qZrR>95x3ANLp1o7;HNrz7dlsEn1c9Y01XZ^&1ZPB&Zhp587$8CexR_e#v z&ooxquj2wXQzy$l|0~cI(*$)MZg?tiME%(_e0&`CiVB~mSbG`MRSm7I%#UhSck834 zf|!q;EsM!(3#`tY-%R}Va2hFI12 z^6Wq`PB8i3E>9SU521%|9%1x!3$&wf_!1E4lQ-HGpm1D4-hL6SarFVupq*yRCRgT< zk9)nyvI2kH!8(#%AZ|J_3M9J-iMge2(4W{eWc8e9y?-`eyS~Nd)}?Q}IYr{!p)I@p zr)&l}n8Cj^XkX6YxqtACrMl3M&;2Vk?v6Afrmwgx1UK$VsG5B#Pb$@>2}FwR9tFEbiN5mJl>pn6$@&9{BP_N z;Q*`UH5}4}vEmbznwX9X6%NkejM03Vk=v(ZI;}aK7F_$4%M5}PS8+h(U(T{4o3-Jr z#IVfSXpPndiPlp=adXG1`S5{q=cEdU{KL$0%M@~tMWi5MI`Ll~Xl4>Dzq|r7W6C1> z#6*Ci0G~4Zj^Twona%RD_j;H?<4QFSI|2Gahkqbx>7O+E;2bEXY3Mv`$w*P)l|sZm zx|51#h8@By@v zcYxc!^!Cr1b4<-%TOks7&mHUY4attN$J+{mo)ka7A5_6jiLK4$O?gy7@d0dshyNRN zaSzvlYT#(3PbGo+*7P^aM2|{vQwn{9HoTEfBRy5+WUFr;<8f*&EyKQj=l6eM#e_B} zirSLv4vCgJjoz%H3qEQ4Wz|x-e3-$3ri06CNIjM9+2___4-)D{!rl(3LF@%-u=ztk zZ2)Ay-mixl=AWJ|>Kr^%Ut+R%fAe_0o-nk*CG9m>S;7=fU!N%2$gKB)b#_SM`$ znufkPpZzmB-Pdj>!+t=w|6Mo66hkv2ZN@x8ENI)EhN6HRde)S? z)=phVo=dQJY7JG^wH|&hIkhzTigRT4ky(lChu>&lJHH28==%Q!QN{>}(hOiIp|dl! zV}DyB*Vsj(wiBDJSeIOt^Ix|qpAzje;WVpED!jP@NEZJVMQwpAXaTL^KUWHl&YQ;!w?Aat&eGnL)u(sQp* zJoc1>G?O7-@9yGnQD_Y#DnIfsms@oy4Md%$sXIKx6BThb<|giw~MFZk0%R zskv1=Axv_43{>yZ-vaxG^N-x$pLn!bLB2^-x!cursC`%Iisev#kPwwv+a)tvlEU5^ z?Yjp@ueeqGgvj6v0eT6%=RnObz_bD`9Rl=ZX(x>{8AdVZP%jk#^CFvu*^I*>qX*S* zUy2{CX@6zH$k1@Yv5Q~40XXj?R04MDZ*o+H7)n85qhwaq|7+_lfZ}MvXi+?9u;8*J zxLXLW3GVI?+(K}d4el14-~@Mf2?TeCKyY{W-Fch;O5OY3tJ>PCt*xn^p6UL&`+Vm+ zr)GsKmyJt|-wRvMPi7xgicKYmc8+y~yw5-`{eB&o{dYG0p9skg0K05aYUkCi|H)&{ zAY)DkQ*eq_i0voDf{mH{RB?N`+{T3Up3Ehm!~gfO(GGU92}7sL8j8dB^QkVV)8dem>_v-KhKdK1kLMKDs zvXR{R*sR4OiXo-$@ao8Q15nVv0QA3iiq)_=ZN_*eWYzCPoLeq$`&RKH9of{@z_zDw&z)}*`vsMc z)TfZLs(cN26#f6)YL>pm_CU;1@dYUtJf~<=H4HM9&neK(3BV6s$X1(XVtu3fji?6d z@n4Kx|DV8$&kmsZQ{liW7EU8Z3cc3}>6;5D!^xwL+r?*wgA`eSW^wyx4OakR=Qo%# z?*ERHlU#5jM*uS?&mPx-i)EEVWt0>J9F^N^TsBTe=bfdni)prSX)KZvd$IQWhe>1K zr^**xsPXE*48s@bwUG!S@WL6l3TPH11nG}E$ugECzsajoK+>42S-|U@*ekG2*sl53 zyIMPJi{x<*?6Pmb5)4nUedd1&2BLdo7kG)}zC-;2eg4pQd<{LeC#$IRgd^G+F=>x< z7MiHXE^c(*XfLprV(bAE+xfSZ#)E4yhvOsDNdb_YPaZu&j?}!HRo}`7sr59Bhq}6= zIb`*x6{(fT@%(bWe-vGXUD^CkWkLk7qJ@j&g0mGP6^LRjJmX;)QVk<>j``_#sDnej zPFsrCMRzF>-BbRQ_HX|b0d#-;2YUNLFKJ)}Z-5cZ7P{Fj^MG1S>vUmxJY#7?le5Np z0aY`B`WVd46(3i$A)FX+_uqN?e_eS<17(K-M?9^fT;7}C)BP?Z5v63zwHy_&x72=m zroH*n=q36h!QZ=0K9wQa`(L<&0kmIXF(d~D(5eFGn0HgUG1})8$I~12CMJxHKB*CK z$nz$$4&*0XI^m7lBLSL$|4!79?)!g21HRB>*%#v%fOxH-iD8c|I#C_T+ww(NqJ8dz zoQR2;2s!$D4eaV3hM(Wv0&`)&HEf{;L;#m-Ffw%azgzV_In|Hn^~@5{uGd9T`?Z`e zYmCt{FJSkBvYtd&`NS-*d1FNN<-e!naSy9WF^0JqMF6n9H$O5Q{`mq*%DkqhdG2I~ z@uBsVe9?T1)n)Z8hq=nDvOY{p&(h=n>q5Z@KpFzZx6V^Ia!MxGzvgf~7< z2h{(EN($L})WFjJ@GaaU7v(AVm8CNW3|GO1t(PnR6HR^uE@1TyeXaTAhH!T20)iV%qMtB|1yrrs$hM)^;5NmV^?zgie*xHJa4BXI)sqsV&$-mY zgL>r{`3&@1NNxEvfWhz==Oh#*=}yN6>Xv6%nd2r5$oZFE%xQQ5>q5fTF060I;p4r& zMel0!@ z$-#5s@Oc#{VNF|xGseZ8va8CLb6xLu2e4(>l zwkz{}-|b?@YxaUyJ2hg`f~x_hs=lmu8ev6k0&1m~cvt`Djh=`;SwkijMu=_u!U7tz zL1ZO_=hL|pHab(GyL<-J^YATuxg#ULs(trha7+P=NA^G3qhAKIo#o`c)*Z&b%~198 zuJ4%TCfGRpUj5l@hlv>ji9qdRka+onYurKSV*7sgS{V!@g~bjo;5HKqzEuT8n``jW zXSYf9^j;UI=@Gz{kUV;Y#L-X=*-DN!7&9DwT%S~cE;8MZ!x%_}w^4WeghR&khEagj z`Bjc~ER44}0%)Hpf4{EI8T4@2zFmirf%iIpY~so}taW*3fj6S6jXIy0be~ohMRBhC znq_CBC!g(r<3(`Xz`jZB*j=Q!d?K3*Bvq&1KAgP}h&2e`2y28rd&$`bcPN#!3Q)o`4{?9^eS=b>Ea` zh_Qdio6D!es4Gq1XFAca4hIHb&rNYm@YCL?$2zos<)uP|37z49BT%)#Or)>RM2mL6 zNG93laYS{4NnMRUsgjt_^QP~8yy%VcB&@WD7vawfaQOf(PMjEj0BW(7xhxFL_~TxP z$cxV#vjIkKCjE1g!XGAe^YQPE#Y%MZd0Bp$@3^_Iz5wXJgJi(|4va+emlGHqcl3eA zDPQ6PMtGT)$JVXwt4YJx&AERU4*~+r*hV}>Tk4t#bR&|^e7_9g2F15M0&dW10w8@8 z5ehQ{Xmx;IvONrrzVvRfCH|$+wGA5BdQ1JSfMH_UM;*0LWJN&l;zJIS>9I~$;)Zc7 z0G!1^9;{xv{cd53{yCEm&&m71H_>oFr3tv#r99^bE(C#q6Qt+O_U%~Dp6ksj&+A?A zLmaj;unT|Kyy@_d3VyZA3$gF;dfo+~slaLRaIGkC(FZ+1f&@H*ArQUNfX6Z5YW{ew z`|-J%vE8`m+z{`9016Mkz75h}eg1OBjid3I@Jx5b#YirT!sFyYId%;X-V^8nPRD1>{WP(;QObF~L z=~iW8+At5wy-r^HyyFIx-P{$c_%$WJ)l6B^Ohw#92nkx%!y;(uI}=>eL4n5 zO>WeS0x%};Bh&$A+xBEXHhJ}~^UR?edR9KU_Hz8A`SKKadLVpuf&N-dP~C2y+z87d zaA-T{xKz6VJiZ$kaJsF-G-kta;lp zhObI*ZK?k_@ArYd{2<+T^IpaP$T0hL`Mc)msANAy^DU-2xzL`_IS#wM4tMj<1+Vm* zb)(o=^knQB{&Q(~bAs8O$9h=20UbRb15=(b z?v6ezQ-P~ozEZyUw>tvHU7W3Rf8_6W>=RA_a_B#xlFfx;$kR1O(JR{6DK_cN`Pq~% zv@uE$A|?u*m-rGrrrAl2tO@qI2d;^3&p$Lhm-C(*#i+&=AgM1MJKa3gZUubklq{YP zgPSk;q%n$aR2X43W}s?lMS`z#7*Srk3-c;S}zP1$KfF!eA z38(`3m!xmT^jf9&hsLzHE+=og`kaBhX923sn>9mw_o~HNpxiiy{$_n%Qwx?P^O($p zE@4pX!dW1{$65^3Y$!z`YU6sR%8RwxgcJGtP{ zS7$3@pa}^{(24R2sv>v%Qa0FOE`JkFc-z87V}}dUI{T-o5B>q2|>R-N^+(VLA9j0z{)Se4b;Nv5n7ZW)*ThUH@Dw9Ol9PbdYsuPHH~j z`+e(@ebrI<5KPQX&6iw&c%Jckw&M*bS&FXR@6(U4#lJX$2v^@tIG5TMYC|iI4}tmW z0rf{wtBry`c0b@1J_Ir=*h-qq%nr#Xl!=;1CY8L^(`Mwps;dwHwZ%L9d!8#d-6Dyw z8i#XsN#IyEQX35Go*dArJhtjS#wKHtpKJzMR9iPNCAc)fUy?|<3e3w?4YYDd9BN-C zxCM^1x6vSrZ+i`8d!thNMs*)*9BeJ0@UxJ77*eBO&0>HA-_n!^bT#0)>H0unjuQ?G zU@uBYbvw*zb{fntd%5%9>+81NtBaQQFYgNM8!OwfS{@csoH)1h(3(mD5bnmA{BAThKqqH`RzT z?=qg|KuI8+MZ+Ed+e8f!lo~4n)xM0`*^kp|`*@iAu~b@_I7TJ;I@KG&nz5|Tx**rZ zZ(OQAVo`hx2*m%w5n%XaoFfmL4* za=hmE8~Khv!j63Gr24{FPW zAF)!DCSy&8|8P;00*(s+CJ5ZhuZLKLnE5`|+RF#Vbj4L-}I;&7Wiw zF;ojIaEfaq6X z^6zms#Mx?ly}}b)bUT9VlyO-!L#TmPd}g#Zs}Xm}*3Z&5C*41y=}kntxRcl?!G^Cf z&Y86s_{CZ5&)S^`$|ZiUOW$OWEJ>KMj$n3!tdI&mE&&&NSSK&d*4#CC%>d(hYi6M*p!22! zPYRjn)X(Vo674B&vJ7^F@&caX=u1HUCH@Z$(3MP@astwf*3%O81yU*MKPAk_Yg3ro zym*^D&jyK&x3`W5W=ETvH$bHJM(J|)e69s%JZH?iQSu;`Z+d*X=Z5Cw7*vG1m^A}e z+f(bkMZqcO<57+O+}LD+?r-G>qVxB+IX06(H!1``n3(Ej-x&UJTFM~S))uGXuDXTC z9Nh4MY{olg&MW6J^n0&1m)57fenZ>6(WqXN=rip|gvHL+%BL@<9Htbn3Zv z*z05FS$4b4lWC&9Xfg2B3+c?nU2{sidRwb^n*H*Bp@bCgj{(g9fP5gD6%UySTV@x| z0{S99F8K6^8l_sc8MoaG*ArR%E~GdP&a3}C04I!@=eJ8#kKAZ?^U`rd;CYnE-cY9T zIhFz6Y$UVv`OL1ys>N)>NI!-p&^$IhK~2bP15a|l=NadPCNTnV(18>CmP5hR4o%Hk(% z(7lJR2DJMgW(N!j!`HyhZkf&&x{e~*7ihq&$I~gQ>DgCcy&fuq)7a}HIp0DqwyrD< z=60*G{PP8VTVMp-$b2#PB1%w(Rl=TTfS!yZKub5sm09q!DUQY6vSk3>t-PLcr9j(3 z0Oj|>3*ATSdq&)x!Uuwldx9O%9M2#QKf`9P6BPP{Nmth@JfhMVHj zgnU7noc6lpOfrr!54m;`e7}u<1L#6>+@3|xje$+1+aT=ubE20V;p-*qg+abQo#N<* zf_nn!mD6O3#37d|{QLfVVyQ&uf8J@Tyc;_teFFD0Uj#!JQwVUB?zfo~@C;74-dK+F z;RqFuebvEsNJ>~{nzBfmq5maz_urs30&?a7-Dcn#dJ9rk5q(jC`2PaZOTkoC=(O|Q z{AepAK)i z7BJ@CK_j0oV}W@R>NqdT@u$EK=8mUz3#(7vAW$_hc?2Y!0Z1u!JE zcS{!Up|xPj{pS-&5thkVK{Mhb!<=d^1kOh)Qp`oq#&?IibR0PvJyW7m+JMObt@5Ve!4 z(^{+rv^7(4ld}tz9sX*U^Qi54y=M1638gDXecbsb8xqG~^UrQI;YoD+90c)riG-$x zp87%P|3GtKgQr_6N7-&Y=ufA|^^2o?|8W#Ltp0f5M>7$pFJJ|RLLQ*)&FkRTPm+(a z@XDzd<=9)V2!KudZlUm%(hIZIxCDc3N}tai3&uM@P<4QOKlonv?~7bBgTldqVY0ax@o8{R~Y2^v@xo?!dVtZB}1V4=KE*TbC%(D*>h4~el zC7ef-RM`m_b=vFv%`-zmyPG1z?DU>J^Fd*uWNAL{ZBtb*8?l)1jZb429`u~e`(>VG zS{od!9X4gq;*a1tGR@o4jsP~0oNJ)G7Z^`vh^otiV0UW+j^N`>DahJW z=c^yL^v6Ef-$3xk4DVws2j|Quq>ppHRNY1s6=kTb``0zM7abbc)a6{Rbv9b6L#LiQ zB79cj&K>+3y8GnKx!6!!f3Ok8dM@^pKbQTu2|90MnVIK>wtoRI788|F`?p#2( zBQsn~LL1MUC1C7n98a}$IUtFtVm#VDX|#7!B$koIXm*S|JSX^?Y2rP55ngRd3OTzu zA#8t~Q4TypL$4Hw*Fapd?ih{0`8|U2v3XW*yyuMY^Rwy!c6&r#?j zvdz0k9H&95upxDn(8Yix`W*W=bq_ob2$`Ms6bO@HlWFA}^f;MF^@=M4UCS%m`s73*^?48fRjA3IuM7VPMToGBJ@E2O4%O zX^_zLEE-rFy?JrqKdRID#n|||G=GqR{IjB-oC#~&12f)K#Pl)OXfm1oQmq_GxV-2l z|F7#~>us%Rye?+~21YGzyd>Y|AG3yNcrz+HJ65X~VSB!)^bt0ZM!EK#!Kd|139TVr zHcbW;f~3(a6d!&R9h**@h#qKNBrX}oufM8I!~9iar?B|y_4|8gR-`k`g`RlfE8gX0 zIGWj%-|mQ?NP@)ggK>h?L3c$VjfR0fuCM+ z=yH9=^eMJata+bz0Oh%X9pwY&W3 zV)*kK32p&(jX#EYov!d&N-do?Z9zU&@|crw&HaQ@ut7*_C<7l79d0BYPdd%YIb}~n zYj&%@C*NdH`R0dwp>LNO=ZL&!P_p$P^PY4AW)A%H9Dj`T&T!Q{IG~qC?E%rLDp6q? zH=__Sw`%^IV!%@7h}l~SH}kmSo$S~x9)6hvn;-$!tGzh^mp|d9PPnEFO~SP*0+d$q zzF)j345L-0bIZ1WU>3*PG{~GkGI=H^|J=~>kKN!^W9o52its1$W08{EYj{uIdsaGe z+vX-&?Xy2Ium@2%c~B*JBkEZ}@St!+3hC=OCBG_=Za~Mb8~JOEQcf<8K@{wZh87D! zH7F{E6Oxk2M{r-|d>-@VKhRSg4GM|PwP~Wuf9>`Xp`(_pV}doS$7YimvNv^mVsxe- z_L=iy_yWD$xz9E2ujPIANj+UShz+)va5w3^`J$Cg?!{R&I(g?};+O5S^`Z03x`^}u z->-8!xp&JoBM8Q5mMy8n(a&C{`G^QdBrw#8L~|Jd`ha6Q*PCIJ7)I2Zv(B<3wjQ zgI6K3rGG4BhK7;uq7T_6*#x$d54@`|qo{CL2;Od<;)p8A$LW2!L~ZJEWm9OROJwi^ zp#@VHIJmWkvPILY+w5^vfHrAMcWa!Dd5#n9`?O z0zf9ISwfE>ioly5xk_666hBK9aRGs#G}Nkygh@0nFi<s`Tg_H#<_&? zY<0}g7!po2faup}q)FnCSlx(qMIXmNmbHtqK76UN_o>U$MJ^{g*dh6{M3`0d&N%OtnPd2Hh$7+VY0I`@R-)H+k|N04gU78K;X>Qnfj`vUzRaELK`^By z_-Y;H41Kz-_+#p$9b~zf@bB1y9r=;U*H2#S7e$^A(Z3G!mHshBOj{EoGo~iyf@-_* zr;#kAr4Y&Q`v6{E#cvjG1&=@C^g+AZQzs!FB;mvfSLh8&K_KbFce4#+EL1hP9b=O_ zA0ySiljt!;jXXCnv6==-`u~pqT&eAnAejGc4aYw|_Oq$e`1N77L!3S9)}#sZ{5g`1a`iI1~fSQ<<=WDy8bwXcLe`X3PX>iTgZe~Fowo}rSes4z`( zeOk~Jv{ZLXnh{g*i2n(B5jugya|F#TvPvs^>$l34zy88_UIw$+n6k$7C5_^9x!<|TXq+8m}8^z9p2 zmP545QfCkNjJSca~=hSJzs^+z_u9%PvU zsAIlA&7zM$niKtT_&G9dry z5Fi%LhrV2vT`!v(l=~}Q^~3JiO=XenD1{+`lR9638nYFm{mR5g@IfcM`cLH6hi7eT z4~|vcNblg`Vg>MvGpAJ; zt3}=y4&w3V*|9j8Rh5PdQY@b&hyz}J#rGbh1C^j4tzg`+V&Z%0wZ)!gLW@7hz$%gxM+HmNILDk_pU)*QT5!!qj zunc%^Cz2o%36uIxd*cSJIA4wj>2pMBQ(v^}475Md1(j)ZXy_zIKI-qo6Y2Y9UHsXu z;z6}VBrE%VWjfeotWf(_g~BVo@av-O&~F4S7G;Ga9jCQZndPj{t=uQ~2_(P0u>9*{|#1PE*i9(yYrw7HH?d15FH?Vo)a(K(pWz`_RJRF*hN_$vtEjASzJS>aCtyk z840-ykIk@(uy(M>&%agd#sjtAXMg9}l(5lxjVGwye5k>@GlInn*TcTQE=V#G&vN-?=I|iL{5!Wx@d>Q2Wi#BP67M}l*Q6y^K zlDEE6d2WApHlL>*w+OP&CWsXb)#iv>9p&-={8y{pj=ar{I7QIXIQn^7%eNSYcI9WAP76R)($Exa(B`%|EN=T+BpS_$e!d+FO6;`ak3Homh(kxwc!IfgfyVO?1_|@ecuwaN zvXo>@X)+$tZ%f&GbH09gi_sg{bsk1MDbXCf-l=+VEVHF6`*5+G%Ko4jG{zfs_Hy-d zwU?@{&AH5y71$+S$79^9v~c2+Dd(4W8*E@ferGRjn{L%@8lxNAYEbR!v|!+e7cFS% zbC9m4q3Gn3*mJVa*3LEur2f8OX@)D<1=MQaBm&ILyRE=|vm`W5_VNpOf0NJVmPzh> zds(l|nv~+nE>WHxtA+?=9(VLB=7?(gm3?qMu^C=R5dP}tR(oTb4nIZ$g&ysLi|qoqrFH7{pKM84y@_PT8(-}oTD{jT9LaJ1?C^hM#odU%5_S$jPY;9 z+1V+RriNtr%5ai?hS@l^$-84eR(@N!!Bjmns~$t@Wbog6c5X6gD`Ql~s}K>5zKo-D zyxvRu$lKu2evnM_sd_N1h=}-$BzduvMfLAaDuC<{MkHe4-0@p++$-EaR&73P<0*QR z4Ph*KM?o%x$yYy9_-dIKjYt?V9UQ08#!oEI_-$$|S{3_j(pfDVhbxN1?&oWk(jr*r z6ij%MGdy`hau+4`Wi#rj22p9JGpn{;v)Gva*fSsV|3<3GaR`uaURHrS?)$-OY__Y}#8DR(MIFt<)4ldiAI?TztKmUJpQ%1XnvD6v zQm~?KjL;oak$QV2XQBGz_`C`RTb*H5R2`1#0le>^lQT1F{6&C1OgPS17_yF94dw5E zWUG@uu2Y+l@ocW~@d91Nd|wqjm%e=GIc4)1k(2EB>MSsEoy2}gzBnC^>x@a>&hy1P zN9t>{xZ5Lvs9@8ZZiMDc_I>%*IqbwgpD0az5n0^N*ZznT=6w^TtXyAN^V3^j^1X6= z_KEudI-kE^@DTa)iT$D2X8oqi??b|LHiu*|O5tjdwrVb@wDRsWt-b3LO;TiizeFjY zbl^IZ+Aiw*mY&q-1MaOQ$0?jyO_c;pp#)XVPGw{(y@gVEg(<~A<H_-%GK()UCB(;Sy=e@$(R~7c+l{(hI+HeuZi89$!WtXa1N|6(EOjk; ze8q`=+dmljGa;H7qoyL^tM*>qU#}feNeo_YT*qjLRb$VVWVD2|aioSo}rLCj-~qF^6;+?1@hVj?#Zp3D^IQ-x}xdXL_rW@28 zqFwMc=G-^ZosHikKpoyNDBXX|cJuSiH_FJI{#cfe6dI`0!`rWwY1r^f@u@TgOBb>b z;Alse%Q1aUlD;>_(m_H(P%rC$R5Y{@XCv&uxlHX|xLRRPWysWZ#vXo>Wkq}1Eay6E z)$ly2^?o7j>H45!>fZ1~FZk7Qd zrR-IqdR%*Tuwq$Quvg)`*tv%{iIj3XtW31hEF(w6ugl?d8|h=57Y;frr)CoV(By$; zI`T$2jboX=_Yt+e`LrFOzCwAYzZoLu=8e%p%RAtStA4`s7ef#2CzE%~*H7BqH`*YL z(JTAXw(pys*6Gq)Sa-zIDP9J6qFF`0WN&umOFO>*N>~4B`hs;mO$O<|uX|t%377Mi z^a~RT@KnXj(8RA*+ikw8Dyl9!)2%Stej=m$1+-IYCc0%&TM8 za1R;LVgQF+Y5(u9@`g|gWUSqJ_J?Dw1EQ(dC(Ysmf?IRR2~aGj&z&2A1y__mx5u1F z*>7p7HZ1`AesKAN!#wOD7AZAbAv2P?9Z`5a;~w6JbWlR~%?Zh~_hYrvwCFOLTWIM| zH{5#6d#gkx_Q8{7X&N+4PRA(t_IMYC>e{WIPx8Mok@q8<@t&2@t3|TJ=o>!Iu^Fqe z$wv^(0}YBdKEA{E;8e3apkK|c3h=T!fmPFW<1Zh!+)deJP-_|XW70RwkP!}m?#_{$ zSv;nAlng9q{{Es=5avqfQ>>{LY>qm<{91QbXQL=BrDwFEW2+FY*@=>6ka*T~ zo%eToUy~;G-Cg$Fx$x>AUj?XKThVlNl;dhxsCzdLl{l6lO$Ke?vqX)Pb=zx-rZ~6tw=blp25y}WiS>S$`1ZLIX3z#9M9|rA z)IuoSyWvGNq_oL>?x!1Ws(OMG|GxFSE>_Vw&jb9e_Z{m5PQolbZAsQ69egyOEcOSJ`4qSnG1c1~Oc^3fe zaPR^;F18ivUjO}AO^G$>4RmWn!~Ag{U%=cja{%vdifd7{=KU{bt1re((l6j7`TipV zXm`^7BxDTawO!BAd%{0Q9gg7@QqZEXK7cCv22yIC0Mz6pL*USIWUdME}%b6(ol zSBxLk!4I!6z*pF@sZG#d3!6X*v>FUBgRdP#fvoPQdf-~H`vzb1;TJm#G|V9d{Lq&J z7#$9Z-j+9uKmRSi36KoFoR0(V5&=u~;D(1o5G1Pme*9PJ->YHXZfJS8(-qJK zfT0V@ugs}{)PszHoynImcHO?S@f_$aI88I`a^3dacVBeP zp{t%FXS(_Wz?q%u(|Xsv>+A|)*Nhx>fdGX1<}zsWvr+ojpBa9UJ|Y?=kp zFD!}Q4NvUB0HoIFX(?JsKB7XCA{ap43Hh7Jp|l0RQ))a10y45e(wxL{Y^%kNHiJki zuEC3v(7v7fP=x7~upg+FgqQ}MNS^x>6sqzEY)F4)P~kZ=Shyo-(7m@Po4s7 zX38Qfe0@dlx3q_UFpt>;!7#-RviQ*?bodu8xXCzubwc!_JAH&bFON$GN&lA6c2t_ z;#>EIXW)iz6n^WL4MB9cKh8?0<}7KmGNS2x{QHFAPg;`HYVvZMRcJ0Z4HW-Mq1>Y#OT*HA~ZSm z(~l#cG@g6uKQt-R{Kf0Tt%N}fSkdn)x@iECOlP~%#=`pM$p9x;$i6C}RXH57JzyAk z3+8WwrfSPw!Y!{Ra7#W?v_n@qb*hUt2A7Sh>SMAf;Hvd+?K(HyESU}kU z?J;!*r|*t^`Y2jn&3!R-z^AyK_rw~vvJ+Ho=7lFy(;-lWGinL?qO~x*QuZ;7T7)x@ z-%U#eVWMcTgoGsd16cz%mxMjvx@&+U5!UQpSZXM|UUMg*0BxAG}>CvU5lJ zSyysPae6jo_QUCqo!R`gePrTAZK*jgbySlKnR%jdd?T`$&b@wJ0U@Gb7JDXZAvSvQDcK-pxN)K6m1xDoe0Q@=b<0pc!h36*b(g_oV6W z8|HCXDlguQmGa9R-P0f6i|7K@E;p}Pm+eOhw&a%+5Xm}2W0;>#aozAsm{rfR@K>xO z(=lddakR=bAa{w&8eEUbpPkmZh9UL~z%1H9;U6G5a^CjjrMrcz8}LEC1xo3_ZxMf` zg=nzV#7X!k$u|UyTq@VZZ!`W zjPC;`w~7mMC58`Yqe%&Ms_grk0zM41!DKm`k?&u{kwH8mh#^Yl7J6{Y*6&_f^uit1 z>-3xNG*d17fh{IBxT}|>UP?#4T%YOShRq9#?vgGq`6Onzt$y#ix{5g`s~K`XV?E(_|K75rFcA8OM@tty9Z+TI|a@O!*8XpsdcUokJ-ifEdBV1glK5T-=8B?Hn9`g;nb#)o} zkmjGz`Q|688wwZkpsRS9M89Oc;1|QcrFgi->tZ(vx$@x+`V->J`iJQ}p};CvBVTQk-rEOf%r$4`DIhUvnnC6D?h0?)?0 z8p)^fLCdCrkAfw0a~xiy&~>_=v&IqFnGui?s(-Vandr7~x-=M_(Y7ISltq(IKHoS5 z^9r!|7)}4nCn`UA`h8H^7mC>);n`lNL0k7;d-mAy^Z1oP0Xr6&v7G^2gWt}EQTD&l z*p3tnk%k^}tmHn|AI()n*jhpa*xxepZcK>F`5z8P46}c~ZcI~!fbP7CCc4-8Q-O2m zQlWd`Mc}5J9C)~&x#(7_sQ7f-O?v}~ECO99km?n2L$;Ims@PG};){{#0cL$h&0%uu z?(KV^381_T-tS|Lvq?Ot8^U zkB0F($iD3;h<#7uC{*~#^DQE06V;*KTsawA!VjC6kJ(&V^3{ZE=AA$V?-Ve`(-h(Z z{|`+xU8V7-o8sIZ$ivP@t0)w1i%*^mCVT7ok-Jbv;1#DYg9LaTD^B*16B{6aNM(6j zuONnM_odnNsD+1qR9ogfAf3W3U*|HGfYfNXD>k`-J+PY0rriGKjlI)(H<@gxGc-en zEw5-57u;N()C9jjSAD#=88tLPO*=|%+JJ6J(mo)}#_s4>rbAO;{PjTh$8%U#nKB`?k`dT#ThbLL?fYAL8<%+r;!LiSOdP zniyvKPVxH6PeZ}Of9uf-F0iL0>`-g=1CbmwcJyX-2=%8K65t9g`gvnI+e2}1MKP$j zB;k5FI(RWjKju9>uK8edt?|u%dfok2w6Bp&1K)4Zt0(MYsU~*p49&&nrp_ps^fN_D zEj+3+&B5)d8QLnV63yvbO2%&NnH02b4|wR;*;*9J*eq@7U}|*u7S1)={LRt47~twC z{9_Y$o+_*P?s0z3KHcQDnTb98{zE?bAhiT?_sa0L?UQbh>RYKFYf}f268;t)%zR@Z z$d;7^4&28|cQ+X<4<|wUR`IUabovH2AA_GDlElya^$RYbS9RWF8@#m6GVe*)l$0I` z3R)xBqh+aTB;B8za=a1!(PvnZ4~y*HA13F=%LzFd+KaV(sf+*VFJi|GAhIz}g*%dhW0?Vf^EK6{uNh$J?op%(4jKa#eGP2Vm#iWT$^M>h*Q zwH(yvQS29@S3LI_AyR8JMntqQiAoZ95jn!=bjJSD&qo-nb!JXEd=8TdE&@fEL}73*iF#@GdJRS{5FjKn@8Jf!ikj zGT|^RoBSy$=mnEqkb3)8l~~V&L@*ieqjhqjCd(>DSEkci)ceC6fCNJX(Bs zOI#+Apsk@*`vHABpg_kjzB@zy0#Iet&VL4My==OG_9SVYyB+sRrhD- zLT?h3wn52M#df3_Q^Tk6gj_H9@60+B4+FUxG`A{Cb{C~mutrLn{DoFqZdh|4Yi)KZ zTLysmFbwa$?u^NkbHSp=&9l&fnr)1a4Q!v=+KG)Ol1PX3LdwRRCoN5DiP)xRPErC_ z-%;Q3C$WWwirV<3ktS#_DLHXdQ06x;qSJm;7^%Z>rp^Uj8XhdtknXQ~U`#?KHXmb7 z+RyhmV&46EO(YB5)D&8KiT3#JQB@Qnb~~3M`3|EoaHeE0DzAeDCl#dLa^K7CgLp=c z9u{@@So~mZ`u6xvJoXpy%^UF+N5nOAWaM$85EIX(NcP(Y-MWhqfvJ6o^!3Y=Qcu>o zk|WX=X8Cz*lQ$MSa!hr`<(E^l**1LEO)O%HSc1_GHFoR5LD_OYnmD+8v*cve@ur2$ z-W8&UXHb-%S|Hepsi=EtI&6_0Nj5(+ELb0gs1|?MOVL0ol~k3jBXl#7x(y!=PIFyh ztP({){#lfw{v@V`cF)E2)lmkQ>d;Bz<=YfCN2noJ12o%bYVqNNl$pnQY?VCDYpU<% zw^q?3p9QL7h_4wuQ@Q+kR*g4FNd+T@0|f{Y%JZ#a^&rmh; z1CkzkC~Mqt;9DV#CV_8Q+y@AaG#;OwTX)1Z_pBO}wcRZ!|BcoL;&CBBsq~ zNPOMxBNrxQBwq!;u)ej9=;QExlfgwykR5S=8>BjrH=e7$qrkXY_&rV0f)%%=?jsLa zOBU5xSO;rfVVCbgV*H9Sq^PN4QCr0z3MYN+o5By&uL+1fJoC!5#?cti9Q@7&e*2bWjb5BkpmpP=4~ zl{DxJxV4T;6ad>lOo9TD+?JXtuGL<;^D&kV9>Er+LW_XGfQ#9L>i&yyI?(ItUc@T~ zlW=0wVxE5NHYl6e2j*LW^j|ju=6dqCi=!^*rK?#)f0AY#Vy zIRtq1r`KP18^~L0Ucs;Orel6+={O2*|LR#9x$ZT+;E^M=y0%2KeBV@)Fx-DX5>WXt zL>9R`d*LX<%77`9r#fH9%@Euvdq&;Op8T=0(kg#KQ)k5m8?8&9V(1Uahl4$#We28< ze%e3X9?1GnMP@v?bbq-&9|NCV{61=VY3Ow}R-}$RZJq_Wovy-#FMQ{7C(l?X>gOIh zCT{NOj`Tmpn~E!V(`UC_6a{S`_TNl~f6M+vAh5Mt$4CW_a{YJdC0+E|3Obha$PM0` W{Qvo3@OK|T{JLobf)m4l`+ooyzSZsk literal 74218 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

?{!SmN`Jv=Jq=lu9 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 literal 5533 zcmV;O6=LciiwFpeb!=b)|8R0;Ut@1=ZE18ba%FRGb#h~6b1!mrVtFlOZ+C8NZ((FE zbYXG;?LBQ*+qlyG>|ddp+zsi)aS{l$?56hyFpzUY!bw8Ay=^x*wvu@3*m?W{q?G;b z&x~X{j`IT9Qg(Z5o%VpGdDBQ5&5UG|m-F(!&()h#s_N8v_DxEGmt{W-h3%c`?^s_f z?vzW<$j!4SD8B2d4zPdhPJSn)^pcD`V?-;(-QwO}sjyev&F^jRmiCG}!n3se&lkt< zO(fmEwoF^qwt%hw^7i)LGg5xa^{=|N=NTql26tckce?)b zsr^swf71S!UV{DKOYQ#;XaDm9!!rh!?a;?w|GPUo%kBU6c4GbSZI`y6k-}51{~y%; zZ;&_Yz)>xa^lcM%03mM!Q9OfM79}UDV;SHP2&( z2y+sszD4>DrNr(NZKyf}x(>#PLM-TqY&*oIril~`kel^&DjkDCE@M=v7pREwy&l@5*`9!k`aTSRJEROpw?So$ z?3!j0ORSX22Jiz1=)Kv zP#Ew9^{TP%ILIE>i{%-h`?2jXe#vg;f&ZyO+Rfw6+iFW8wKh3#HQ&^Zlp`Wm+wd%I zk+-$ZY4f5(0HRfGbl#EXF{w7*k^j^hM_WYs^}MCD+oahdwX^ejO@X>vOrku>%7|{$F)uaX&wX7DmkyVI<>=# zdbLH)FIwl#wgT*rfNZ1IIBo$KHX z26p4H+32+3aSM9c>O`n-Yi(tVR9m$+D&)A;1oEg(z-TfefY(qs6;w5u84&=%=Zm&7 zmE}mO)`3kKHn&~c?`v0l_pZfo4`Cs?{TT1olOv#V*r}Y1KO2u;0|KBYYQvd&H z@_)|ND4GC*5Pw1+RJF)^r$?=;kDEi!8@v0mJOC%!@8vanB)hIh$3yUcZA)fQIWji= z0rSdLaBv;_%FrpJa>?3BNc$3;%F!tBha-mgOm93#1rHZzCg~ZL>Y(r0cLE3bee2EH zwA!I#+ciAfnM~z_F60ec4q@aRxG)y4Y5OM`r5)28(IZT`^qmsoLJO+!P1Gqybw(k(l zcN~aai93OXG1?~=Ob)^&Ae&5{+*0)g3g#VEA<*piDI<5$swWf(YUm!+8V~>|?e0Of ztyIF9Yd}=)SgM()oB^A|AU0bqkE!;1n0;4c5NIC90cO5o`7TWUGAL)|v~_wn5(itR zL&GvcQd0{yztrMj_bxZna6Oh!ftfOEBh@R_Fx8$(!?egOC#E8-4mC6eV+eI4LFk6P z4%Lhq2pj}t2Fk^pPo8vaa%Ir#7$0XAZ42`!JiDOkx%X~o^ZSnf8uj`=AVWXxlh2Shqn)4Q3#vBbQG~Hc&=v2g1EIvXSmY8RtteP_hU<;jt4W$|Rn2QS38z$0Q#3;AT(*W?=W0q~Rs$5OY!^>{A4sh;3@ z&2g=+bb(y=jnZo4c}0bYB~cIpZV*yfePiG|baM$p4$sEEaIi0eKnVXe4>6%m`vyey zB9BGHEwVp$jFAeyaOE`wUOr{0L)-NjA|d!IBKGVXlp}~?0Mhj)yE`HW>VcyN4vjkQ zx|(B*J%Ie>7pZ;Ns-1Tj0D%|)PshlSa!{)_y2q_%qoXvADwb`rkO{&A_SO(JGCxvLKm0dys|1Y`6`Hyk33 z=Q1ooyJ->0C30|I?VdagA_OBeGtewW#X=s@9a7W8Ibv8Q&Kj5^$&uz0=n7;UKvV<( zebsbPUTWzA{!jhC)c;HUzqEX({vQ}|`P;1j_1^#4Ef&)Ee^UFO+W*x4r{#(5KXam^ zswc4j<#IW({$U#K?xpenlf?heW#4t!z+*(#Q{ADk0kMADA7PxlPokr(*|R1H&8X&? zlSRjacu_PsPy$NR`aGk|PQv$-XKBHy&jty~>a+1>V0a?;OK>O^gGPAH0CDR06XWIS zK}7)SE;zg3$B6kUODxCJ9ujRxwNLDXY~^8BUJx*$B2nQzks#X4v<{TUo9>ZvaB(7% zkL0JHSi(iR!AwhYj7KG^xlgJ+jJNULGVxJygoUQp5Umr9oeuIg!-Bxt(242{*zIXl z{3dALR5eNtn`dWuxs^fUnT@56Xmk-fxQCGwj6MO;K7(jQ5a?%ww1hy#62!)1&>@#0 z()a71Fu_1?AdD{xeM1oD&!scBb9MWYxa6*ecsEPoyn6Vb>WR`lZz;#MUz4$@#o&8amcFm{d0(*MFbag>cOh)@;NK8!^&>Z?0%xjVVzDW3BfTIwvO&VbYL-KpA8avL0O5G%qm>}eV*>@s%P=X zAi)@`skRPJ?_@DSH4rbV0NK>*Pb7Ogb_@$MqNaUKoy|-kcNd7r(sYtrhoOzIL_ix; zthY>><*1n=fn_gthW3y_h1Xb&#kW{M?u&zW%LUISB!9?Nm)Wr-vm-|~=f*ZC z&RcM5xxPcY{B7VaG`OCNyVcaDg083omLh~P`Wy@u69dDkGG}n^L?PTfx-g2}U=+LI z-GP}o5oa3MrWIq&O%>k*2w(T09I%=9%E^3-gu2BC_9n77Q}eYpbA~2=g#Zs5j#>NK zH*UIMHM;{k>h|yi!SA_iunf@+r?;M_m|1e@$iAXo+*@@zreFar4ZX^TQCep%%nS)% zn9+r+q-~hLdKDYr7fBkEb?{lQ)BI1G|4H*dY5Du+e=wN%o8;9%SAG9$uTW0r|98si_rIPh|Bt!5Ne0@;$7xobkkJ)p_ZU>)U_vd(yeyC@x)wK;D&7Cn z{XgCR)AA_$fASRSoA3X{onqqqpQU1HrT!uvDOJZ1_O)lB*!M2UNwwU3P1PAo*$#WvrKW~yPx^3}0_<{Qq^+uRpyU*uQaWhTai-{c!tQh9m zQIi2!1V=4A9{cI_)MNbeg>g+F09P@3?*Y}|ni6`8)L|O<<25Pe3t1p(*^;B%TJW2( zh%~+jf|CD`_CNLiQvWaY|5E=i=KrzFM324xSNVV2dvou9mdfSS|NATazvIa7bEwHa z$O?WD-j%twolkw!zGi+7Y{k)S6#lsvytUY3r}~7#Ha=lEGiUpz@re?bJ7I1}m8US= z@Pj4cM5H=i9K|zs4v=4VV_?R`V5jl%REdu-@_k}R*oN+2SHr71!Bw(pT4#ev!`l!n zCJE-gLTr>X%P!0^j<{C1XUZ_Ms2d=~6%gGi4L(_eS4cZU_E}f1V#52xI?g=P9`&yA z`ar~u%Y(O_P`YVVURc_*)e7O&3^1%0gIf&z@s67SsNns2!Fwh~K5?C7;^;FTFoVFC zJHeM=9hw2niKJ2?;!^F+dedZ9%FL$eCdkFyWeN1I(S&5+uk%#dOm%R1C3IJRc>ACF zf2seM`hThaH{<_JFT6j2|5x1I-rY-{|LvC3@BjbB{vV$Mc-=UB!zXwfL9=!4lyGlC zH4W7b96)_Re0R_-gN^wnJ5|-_x|>9_^_z|4xHDJ;j!Np3w|mirA=c3*Eqt zjJ+$dnG@LIqJ{6^1a)0+GN#c3d;21M2oqJ|v+8I=A^PgapT-X#4Xba1W40N7BGqTe zlA04^KuEnJ`WAj(#fmo|@Y04ZyI7AGXcvNnFI+5YLm?MPHDXe+E4j(WoX9WY60%u0 zEdS=qN#o*0=(x@HgSCf=NuwVnHSzScPh{$8vg!6D2uj%Z>ONvVHUc_nDd*khIezWF zT@gj{fu%ylqguPusvUG|M-~2sIdPuQG#waFi+P|)l?FuSS|g=pHt{6}={Q@-l5D0} zsYD{Ql)6%FD`N0|L#A|>os%UuPdQXM(Z`<#;O8=I32P0UyuL$8ghkEAR~s9ZGvN7^ znqwq>AibTb*6*rg^GW@cs^g@-z4n&rK{Y)S`w>&d{Zdy!)hk2>KSPrsrqlIU*8dCn zz}^9Bv?`S%31w~^gU~^fbexY~5y}+Byof!zw*$7j-Z;Ye*<-wn;Zkvq!a76O1S)MG z^uNy71@Gp-ED~=AuHV~~MR`jUb3Z4?a4Hm-*^TTyNtDVEsy7ES;L*rQWVAW!i(l`poKfr*oX3fy~U*Cf}+r zxD39_-F)oWXkxDnjk*t7kGKXzcVgK+!*<04frqU^9-bVukHls3708?RVDfiycluPf z9d)hoJ!U9hV`CxtFQWLVomC7djV$H_f zxTje_>=Y<^zn75Bwr@4ti_;UVNfZ{Yf7?_Y!%a5EG?z%2T;-CmOEN3t+~S0Yn7K`G zxf4m{5AxgZ_5^0di--C5Bh|TQ9VNMJydZbWR>D%5?2!ew_hB+M0rn|GGG4$+Sl(`! zrayYH%n;<&EFC;x^;-+h-pw$y`{at477OcgC>9@1yciPSz`sF(>zBED!Cwpc*Rn-+ z>`)53>EdM;+h!9x6vuk_B6D)fVUvCT$3LVO(O`sf;<&CaXMpu*+6UkE#b^ROSGP-oBVS!w~I*KJ!~(Cl&?D1hPAjuylu(2w|`wBpL@-o zz@j_0?rYwgRq26cq8+|Fd0T5NNuR$pEZx3#7Ylbd8N>t+>7CUYCvX1@xiiBWTqmhT zgd7UdwMB~YcJ}p)B#-HM;j=8iCm-aGa`$fjC}RqV&&wg>XB%^q0s+`$@EmYhs?vWy zX5ptQeg2ruEj$64n@@`w2YdHz{CN#Xb)@fxe;(_8Xybjcw7pZ_-Fx}!)fxoS diff --git a/testing/make-archives b/testing/make-archives index b2b288cfa..ae00d60b6 100755 --- a/testing/make-archives +++ b/testing/make-archives @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import argparse +import gzip import os.path import shutil import subprocess @@ -24,15 +25,14 @@ REPOS = ( ) +def reset(tarinfo: tarfile.TarInfo) -> tarfile.TarInfo: + tarinfo.uid = tarinfo.gid = 0 + tarinfo.uname = tarinfo.gname = 'root' + tarinfo.mtime = 0 + return tarinfo + + 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 - that is created will be called foo.tar.gz. - :param text repo: Repository to clone. - :param text ref: Tag/SHA/branch to check out. - :param text destdir: Directory to place archives in. - """ output_path = os.path.join(destdir, f'{name}.tar.gz') with tempfile.TemporaryDirectory() as tmpdir: # this ensures that the root directory has umask permissions @@ -47,8 +47,24 @@ def make_archive(name: str, repo: str, ref: str, destdir: str) -> str: # runtime shutil.rmtree(os.path.join(gitdir, '.git')) - with tarfile.open(output_path, 'w|gz') as tf: - tf.add(gitdir, name) + arcs = [(name, gitdir)] + for root, dirs, filenames in os.walk(gitdir): + for filename in dirs + filenames: + abspath = os.path.abspath(os.path.join(root, filename)) + relpath = os.path.relpath(abspath, gitdir) + arcs.append((os.path.join(name, relpath), abspath)) + arcs.sort() + + with gzip.GzipFile(output_path, 'wb', mtime=0) as gzipf: + # https://github.com/python/typeshed/issues/5491 + with tarfile.open(fileobj=gzipf, mode='w') as tf: # type: ignore + for arcname, abspath in arcs: + tf.add( + abspath, + arcname=arcname, + recursive=False, + filter=reset, + ) return output_path From 14afbc7ad666d01116e4f7bfd1890facf10fb849 Mon Sep 17 00:00:00 2001 From: Jamie Alessio Date: Sat, 15 May 2021 14:22:13 -0700 Subject: [PATCH 519/967] Update rbenv / ruby-build --- pre_commit/resources/rbenv.tar.gz | Bin 32142 -> 32678 bytes pre_commit/resources/ruby-build.tar.gz | Bin 68010 -> 68689 bytes testing/make-archives | 6 +++--- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pre_commit/resources/rbenv.tar.gz b/pre_commit/resources/rbenv.tar.gz index 7723448ec5fa447c5a369257fcd768bc58093f6b..98b7e0f60d5437171a82991b983ac52455a328a7 100644 GIT binary patch literal 32678 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 V!V{kGglEIg{{j9RBvJs-0su=|IMe_D diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz index 2d8413643b29c326e7435aebdce8036ad0368f92..afec5dbe5eab69fc879523b3a4ad4468f0a62c6a 100644 GIT binary patch delta 59726 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

@>1otlx;d+ztK6T?gd;&@G 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 fGPtF8mMi~HvHy#|Yqf@6c?Z=p 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 delta 58918 zcmV)WK(4>hm;|bm1h5eyf8|Ct|5L4ApWpxZS@l1u&?Rn#>3FQ~*tgs;)MK({?R@5! z$V6miCvZkwsGhLVqnh=xM=0RcIv~URC8N4uUx9U2)$572e*5mm6GJ$B>m6(cs%ZYr;-*?ZCf4VpN$B=SgscAH3sx4W#YE8xnj!$RG*Lb8-uB*Gg;l9oJ&Bu8MRy`<`+N&SOyH z-0TdNer8!WG%gemqukDo;?L8?7%8m<-oN*=F&Z+Hs(It#X_9v=~ChrVG&AQFK zC6=}L^6l=X*xl16Ug`L#0I4FHa;P4%6Z2%zJ~6*B^+*orfB6paj~h$|-q?Ut+v$aa zOM!n{m=NlS_=c%3TDXujxg0m{2xn(X>-VmG^n(tsee!(rhZt+dO#wr$xmZekLt_;y z?U7FnpOW`fLwxLMUAz-YIQhtU)64phpb0E(Y94wq?MQsrc;bWl0a$4)o-+)`bo+@U zP~muc7Mpx^f3O|qfcm~+{R?Zv4)Ut|%45is85fU#gBnn=WVH&`X|Yp0|AxiIPUks( z<()@8;2EkZt^=b|Ef7F8ei9r%i~G-F`0R^r%*C>4aCA((t^64+xe>)3EtE6qLauc$ zb~}QLvc%ffFi$UF#o-7>_6|@wmR??Rh050A>ke|*fBcpNqAtrq)^HSII`&;ysTDT; z{3p(L~CpobhTm@S~Bl%2TV|DJZC@AGo12=tvop+9^`>lOo#NC|rnE*Q81p224v(T#aam51woZoJfcOe*P|TYN>y~U8gPbhqqPy&Edq7uA(IqG7>&D~a zbhIuS(VbyjR~)l+qeddVasRRd*Z^u~2OW_&0Km@PD=KJagtHocrNYbux^5%JLwD!= ze*os^(hv_!Xfkj1wF5BHd*uzUD_YMv_d%HWk4>k2X-OMeM4Rxkk z`tTv8Sn)_>%VWQZ;4>hBB7QBx2$1@|0o2ep>`bkrTliQg-IZqPlGuOBP=S7k?N#C*+Z@-#R zS&fOD)X6XDqE^bh;BOL|z<;oJTFy@MlQfZ1m5G<}W2*1pO%^|iQ4e=if-BUD(TjBFKT ztjjb7k@ht^4tw^ zykY(8lpQ2p_TuUpe z0;p;L4mVSh{6kZ{e^$yXIY^`ts>yY}diB0?$S6d?=Uhp>osOZK4Ajl0DxngL^?ZD> z#&QP*=Hc!1)0K4mmTI%amIsk)d1V!b#+Kh~)mA8@TD{7aF-MgJ3@z1yIJ3otV?u(O zrYqMK3diIGqPPrg5KiwLY`yUXUU`Tstg@}=ta2Vj=Mq9@f9(}!i7D5*$E;Df%&Iq$ z%_YHnQ6)j;GwA}F>X?120g8VdvJ)Su#pK2SS0ve%G1R@tvv6 zsOkRGtd|}Mn?t~dS)?enH>0eLnAlpkFi7MAeo$d^hSW`>YI!m7kPjxY4uqRuU;svK zt*L<76Uyv;a<&TJ5QY<;fbm4+9X~nKQ`D(se=?{;e@&ZM`wpsd9bQ$;fEc7@`J)?a zG#S9RL1dxbPJv1c56j84=y%+g4CsZ+m$V9Sr5jah6QV^KF`$2mv2o0VXN6zr&*ZC* z=l3KCbh^#LJjB<)uL2H01|CalD6qWwuxSgBL1^^V%F4Q4({=c_gh=>A;na%}@zkAv z#M?Vvf9O`FXuoeprfUn}VL~i)5ygjjjio`rH5ZNGS8BwHhj?8$R8=|J?}KBiiNRI6 zsG@rB6>~?B0hZ$a*0CiVf%t{3rw%@yNJX$iuDL;qa>-{wohQ;m z?tOO}RW#)`?ST*}@y$nL!ZY`MIfDx1ikDkX?)z|9X7}>xYiER4d#;JHDq-h07?T?l zf6Zz6O-|0J7_eXgg{cIunDzSxDW5%qLEaSrc0PZ}bS>V>@5T4U-{pRfAvYL;NgQVUJmv|dyG&kM*^lZ(ns&fyCxY@A`F#FQIZA?GQ zZ3~T$3~S28FD_tpD=fvnri`GOf7n^h#m)*%Dkl@`JC<&oP8kqJ601fX@e$lp4p&3G zKZ&xlW?%7~ltsih@>AwcE>z_l)7O}x&!zBrN;rNLT;^^9;^MiJF$2Q{zk3QICQg97 z$>lZ<*Y1;D%!f34C^@awSEj}cyXAo_r|%v5)op=8=tL5s<1=gw*|cd%*`P1i-VNla;e2`_BX1J@U^u>Df3^spO-*6C0`VCGAZlbM9b3SKh%LMcYZF!9%nSC8uc{AA+`!rEQ#2 z$}Tj)I3!Usm%!oO3zYe{cUnM~_&T!-=c% zl0Ir}Qk&L~-!K;jus9@G%9B;xJq~swze9KAo6`}CNukB(iOG`+1>l@JZHa5ED>$=C z!yt53y0NB%E{qO8oRGCpq;FL?@iFa#1V)(wW@>V9*H3QtnP^Yd;d> zMZ=wGl}RR3;gOv(f7W*jQxf0==B$t_t+R1(u<@&@g|v(uOuQ&tJ^tY|39>0`o%SXH z#m3xzMOB37nu24*DKrJxGR1A*QXLHKIDum36l!(QmC*^Jhdg)bm`IKZKW`lDZtuQY zW2s4JbT>;pX5kob0rBx<1khr@Z~DbE$=8%^L~Jb;bZ^1-e;6+!0p@v5nOnRd#G5wU z37SXm@aP9%#wesKf><1QlM!Cc&ND2;g{i=k%8b!!y?@kE4+3YzFC9t^JUc?Vic1Y{ z=6F_dD%8*)H9ASr9eVum*yI5XdF|Ycy>N9C8dQ`;^$_a!vW?E;8nJ$7i_58)#uZaD zK@Y`d;;$QTe|9pL!L72K8{$?O$g#rINP!TP`Zk+FpFOi=MipLNIx1Bkx-U+)d6&GN zuBin><=52;+|#d|@%uXO$fdZi4bE&H(-JQ!m`}P%9S3sSQLpc*c^@2^Lb)LDD=BA; zgWl!&fMpqq?nCU!YLw{Nxrq7!hCg-0@sxm{tp{6ae-s363>0>G4v&OeizDi?A|Kwl zj`uQ3{`RXh@H1#bWz14T6{$S_}PU&YDd3_NUjTgE>?>;%dImdC-s52@eS`m$w$a{#a#;3?(bE_ z6D+V7Pz1CNpKV5cPn6nua83w3Q9Y&z zfw9-e@Bz=v))S^jk|umHN7-P)r8h%x-YFwRb-F0COIjY&PK(N@?gjR9G`a)}K-@ad zf3cR|cGltf;X_gro%n*MO0z7eJC^s=SiGpj(*7l$C&FwTIyWvtLzL#Ah{cL_p-ErF z`V@*u_Cm|TiA~ow@9C1M;bqwTLksM_dkpH(ei`t_S3w`$mhyZqku%TCf3bcAx^^@w z@`q3H45~TfEoD5aGnx>+f=tALjZ@Ane}z@rFUw&{n4%FHt3%e2N+xqJW@S%cAJf$1 zAz#Zvys$)Au!UQOIbi{*=x?#Fcnd8oJYU!RVQCog_;KdR4|E1ubO~rh-C;wj-8;m# zr?x0=dCO~6yvp5hd}Jp$V|hoX=^M^IOgVruLQ>(_EgqUo(}i?ZC`Bf@3semA!VSP>bUq~;>HE)4~W3BX#DukLoL8aa~r^U3A5r- z`2l!?m{@U%mq(pqH0GB(;5T_RG#;ZvTBQHrC&-o%tR-}*EI|iLIeNi38anT1&usq0 zK5@RNFF8}`^`=)HXXIUk7@P@YB!lH<9D9@pnO6h|QT(o6HO18x;b8I(f8HGH=b!uh z{PVkJy#ob-b)we5>-MlbKE(WR$&*Ui)PuzNn?b||;rYe5cf+SkyGl3f=S_mLr)K?k zjYM;2efFG-x%^$nI`5(%`Rwa=^0(A5s8FZpfa-Zc2d#O&8PJzOu3g8pOBfrR)$p?M zMc{(j80*Ik)2+yLnF%1We;fi17@V>beispK3X*J?NEQG+KtzNjp4o;~Te%nvAe ztXvYo;UX;D^4Zc!X^EpjCOZ)IomCky6@wKPT<4ORHjCE^&({`CzDfgef=!$hss5TN zJ2X@{jd*~KWa3n&DUsiMe?MmA&OG*)og7pKMT^BDuySQ9VFokce}v|}0Cdhrj?=r6 zXHw_{dWXwHx&SH{xDM%*X)r`PWdY;p z@9@|-T$Ja9;9VfT`FKY$eRG{LeIr(Yss^^_VU{!2NpM6$!dyjCd!W z#}lG!U`(eo6wpmAkRV6V-8hm?Ds4%|5v>AO`3mAeZbMIC0#nfCp5sj!ho}dy+f(jTNQ^p(vqAKPI6;kU8*p1qD{ovK}QNo}#pTEYE;1e3jHr6!;`7!xdZlZQ-k> zjGzNyKHa2JN-}R$YAl7zVl+l#kpr!|`aqL#hQuhkXPMKhMiz#a+3laH)6T*jn5qE1 znkjh#7B~6{(RTtf@L%6uv2vlb#1Es4{KymE!n~&C#dzY5iq7vKFH)e37n#$rSWG6( zK_@c)W>%6U=%2Q>pCul5_-#*=wgXpdi7JVgOYGZkzcm;2cFp8CH+TiOgeRX z^7T?Guai+X6My7C1yfIq60a|3K+4M!Bxn}BZ`zzkwbbNj@ z_C3v@0V7x0%ruA>Y9tZ5dLE)SKP4iP;1G4LvH!po1&J?OyHnV+3cPrXS0s#h8I@k# ziPvN!c;YE%1^7!nNOgf)V?;LUik>?VWd-Alt+T7%Gr={=oqDjvm zEREC6VJth{V-WH*k|$>X-N~7_P>mk(TPjA3C;BR|{d9y7^p$Qh6(^Ls-%*R+pHhpn zL*Hs2ZGUX<9A>jHtx=B9K#R5z&K!P;U)1e}Ug!{1a!y8kPJY45;)et8^oapU#aB{ULDI7of|DtOYUK+4<=W zb8p500$E76ndt!UChmhui^rhB)ab_o(4<>!;D6ERGG@4QTVSuFYrL9pH4CsxM-PC} zO#xBQr(5$JAe=(0P_c$~xkkzyOtqkw9>4PIzQGsRQ6vrp@D`^!!n;ohe=i(ylYm^l zY`dw|Ow#seFf%i4xoCB?$M9yeqt|V=f3Wvrr~Ss%(|%Ii9pw|5b>3A+MR_6kYq1Nz z6o1pwO4=l}8w0hP*}5ooM0nL2)88sktIQ-YB$dIG{!lHnW~LL(f;5{LxDyTD$3ZS! z@`UYdz;bS)z6=I~V0^8X5mavj2`=%yUa z0-Zg|aSHby#Cdt8JEA0EV}fMFZ(wCNMN(wn4n|oVMNu!#wu+*KxQxUAy3;VHBAY5n zu~pgj6lY73Rc0GNW|Yf05RimTaG9e!B=Q0+MEN%xn|p`9bhg_2JA1!k{?^XX#=)!h z(K;qXlQB9Lf7SNNv_6v@aKRRMTnH(fsi%74pOk>bZ@0>(z*pS2hP`I@pi(W8EarC{ zIhX3r=WJ?PU60TQGBtF%x}0u8*B{3`Z84t^dJ8jxc4Y26Oy7;pN0nTQPI+H5<4TR2 zGRYQ^26bbKCIhpVs98BpFhdPkeB@kou9C~FJ&RM5f2$>a$ybVO^L_%|)-bhgDz+H(4%NMzj8zI{mSlELM;LQoV`k5gwu&vj$4wnV3l`sY zurM?_J+OS*y09yBs#zWORT&F;ugA6esn)SJDZD>Tf5xZT0j!wm6x}?NWl}vaL-#|2 zqtU3|^~Pq2T6v?3h`f`6J1GGvldL-#0X>t(J0VC}U1m=wMc1%iHO2^}33G8C(VS2qoSDcei zPq66X0U_s89$$CLtt#$FQl|f!;104?x>rL2mChJ4i%U)oT=jf!_Pk6avf3%2gKId7 zm+KD0x(a`omi$#4Mo_HcYPEJ{u(H1GqzKqL} z`5=q%|H4J~@a1DBMh{>zlMc*OY?U@@+qMocegSXr7>Su9r^W;v+WG5bn`|R(~PTVa7{vx&p=%Ep=|NQA%lV9n1~p+3sv>+*eoNMuYoE!d)^-j zJ&Cfd{8bq!=mI*x`a<9`(kbe&*wg_C(c9}zp~VXoEMV?ZGuLVP!d z2Xv8>sXkl|NK>zx8ZM;H08Twj&PS6IKP4928mI<^3%AzZ<2U~Xv>H4u+PlHqE0az? zB!82gS(hWXcES zD2CS`a-!yZjUs7jlZ0IcH{OL4(m9dB5GEe*n_uXVGrvAmJCPdnDTF9B^+jwKYtlh) zdG;4c$ig>x<22qIlNUf+f4kCtIxd~+5-x_vxAO7^XNX6*#$XG5$6n*Q3_t?iez;h@ z{D8r{?!xWmo#kW3hr0ql3bw4&Qd8xvhss!(cq5cuO^P4|@%Q-6KBxCGONcCI-e4M| zJP2g>K`$2ov=mkE+-#(>vID+(A#UvBiI3LVv+H9y=}J>1{(%jge-=i;?|6NXAF&Vc zP~NcO2Irxc?v0CW{(2?b9q`B*`Oc51S69Qfi&ozBle6PC&~CYdM0Qh#?yC( z?3PcvLe>Jcyez7$9K*{~K!x9MKht21Gw@a97X zIN-2&DI=KQcj{oW$bBMlVTq;FOR9?mZ!k+aCuse?iIsFlTj1aW5kLt%NGgPQj1C=m z0i_k$+2Tj6DFI)G4fXtP&pD^d2-6XRc+CtZokjKv5{WH}RTf1Z>d%}i|V6M0WS*0%;e!4bw0`oQRXb4PKe8=!Aedn~nc#eefKMZAZvP+Af;9p4YuTbK41 z_N@&28U3gOnW`uLM8!nfg-W5eWF$R_8tVG0{FX((NNiuXDd&Q$zi6idDp!)7YjKlq z8v8S_f2YnvGvY&{S{9b{5r2U-ui9@qulL@NS+M`})?Kl1&hA%0D?Q!0UsULgAP|INtf0dMVF1C5(<{xb7H_Nh^Q(R(tRb&ZT z6+3|sSlMFoMrFmb!c_gtZEk!Eed;<%o^|aijJdB8^PGa{K7OxI9r&K97gU>L zfAkt3&h_jgd2O^-q^awHJRz`R{y=KH|dbFly@GVbhO*R)d5&^lx)E~f9-DLoz6I!`blh+nq9m<%!a@S_guOKu{qa^ zr{AOkV(>y=U=|$}MD+m_yzmcg43sD;eLcA<9K1srg_`KW<|@zE8(go#B43dS+KzUX zKs3|9`(U(<9ZWl;`uY?TaFRJ5u!AB_W*eKXRD7PHg`_?r$5Q;E@Vxlp!&2&ve>`}A zW}!9mFf{!1`Qj4)EhZ)X5IR^~%C3XR^wHsSt*^+e6`D~)IVG_k&Lp|zWO@(aMbRaC zM12r`cq1r=79+#K)M0q7jzipJfsNdER;AEtVYbOh>E~|%7CoVV z0j&NgJh2%-mH^>gU$UiJG2r7x`;@IOmSK_FyIX{<%a;Kfd!hhLHxH(ge-+>@*iP(j ziEh-^7f)j}U%-U%VRFzH%N$%|GC(B+c;t({!?yU9$;&RDs%>uExL}JLJgzRE@Yk0Y^IxzOt2o%(J4$o>bnkaXvKch-wqj%g_lAJ)QlE~6 zPcEI7g?zD7>_})uBEgC{e+YhoQD`Pur9)F_J^~$#R9Fg0BH?7<$HX;ZA!}xZNOGE6 zuh^p=v8<_8%VEL!)i0?!p0JSJW)NUq1y=G3S5yHSb)z_tbp#4!%MPh5sZO$_LZocj zDwPd~S5+|%M6qn4x-3*r3e~3|lqNyW0%V3y8XQl^L7Vjp8wao6e`4q?Uunu|TIMpJ zFPY8LjcTGqs@_RWI>eKNGiJ)_mX_Y8$Zhx)KM{9NrN1OWWi$m8Pq%^R^D>=I<&4W2 z^&=X&C%nf5BVfVMlygY}fzMoVF#BrGg0n#Lf)g6MmKZZ!80 zR3N)+98D+%Z@S6+>*vw>`!1*Kx8GXry_d%S!qcHIHV$9ke;NNvkoEt<=c%)vnYwa@ zrg4c+$vOgx^MZ1Xws!~7z|tm3UO^#IO860Be(xO<-+H5|u?>0XH49r3c*l$Ci+n%8 zuxv;G*_C6Eo0Il-eib9-4B$CDr~QbEz!)ztNEO`Y7aNTZYYm3x&9}^cnpvFWPiQdeaqWGJ~PH%WWk% zC!z?|!KVUy3)-h@YUEH#4?6o71H9fvDAKJie~67EYDyfM5*kq$Cnd|Jmzj zZ=M~RE2Kc}>?NT}87-@@c#HM#3ccvuG-QOrdQRRNkt}2LXXo%rbOA0VEtBv}6awyvlLk#K zf5WTm=#n6X5#n+P_U-20n>X7>*1^W^tM+$LSZ+4$stthK!6*_Rlf69}Cv%e-wKn&LrRRq6-Vp*_}w!yL7x^{p)lIJ{{}* z=~C*>+;lF;1ojdIZMjr^-{aTPC|!Fk}P@}E*DQP}NbUPI7r$e70aA>fe(<;KBiePi>@feLFIMwp{F z#3L)6L6RUK9al>o&`jt(@(>@(0>q!%HrCP(Qdwa-$+!#{e9ZV8JzN7Hi;k0+;&Y-o z`X?bMW?e);JLp0>{>?l+`FeqWe=P9}_jqK877!u=_w_+RXR~oi#PXrP6c-A!xh~I| zDw>BCwP~nf&Mnkkc)lQZ^*Em5vu6v>@6-v?6m8GA30xx2hVYfj;vA2rj)n5VZ>Uf> zIM+)tNgZOxYt6#lu)AW?ttgg5h8Zf+VHK4MQeejw3rI#QdAspu<6mH5e;-zWNp1ev zKKfq2D@#T{=aBmsu}pkIe}7Mdrl_zQB(V;Css}lNu<16S>p9&7;Qg<~hs$PnVG)Icmh+BGnk?YkE@htFwf?0$f z@jFwz7U~96R4ECD7JUKbo~PfYcOR()#b*5X^Zy8T zA~tkBo&H~LfU%d+{~Pu4T>t-4^nZa7K@n`h|6+{MJDe=BE#0GY2k0E5|bM|k)OlQQxelyx`0h^}LCvX=r|MTcvqfh~z& z%-XRHL%Y%a4;i{HZgl69Vq~vy*BJPVa@0XOqFdoaC)KD0w*0OlK zzvYZ6Kz>CJ1d$b{LqRt1)FCNC6jiTnIzywN&oU=wON+O%e}TKDocIpF7B1e?Xk!|v zV`%v@XkC}YZq*V1D05sxSL^20;;mL51R_O2N0XSp<|EBL3Se1|>*7uBCE=>nH2ps& zVK5e%&{b=9g#uP24IMMGlR_|aVs`NarGvNk&|wO>#yostDbsS^{AgFMSZ!v;U;VZ` zeX?MErSSDvejq{w06uyJ;0uEn~l=N6W+%B$P3tR>r2nsv-sKI+4XX1wY*whdG@u7 zDdtC*i8u~sEX;4|l`-z{%uWKQ*ith2R4i$4VD@R>f6b6ok)(EqU(o7HmTCx31Y$BD zEROstO`iyzR{hRQ+O;e?=a6k)jrtktWOT*LT0C*_(DKT&l7s&xFWkt#S&=c>I`{DK z;weD!msX|&BS-#obK(>F|H|cQ`Co06=JNmZ%YSZJe*1EJr~MuKR@sx^vEy$y_jZpc zlkczlf9>y1NszuBQbuOx@C|a2v0|g|ya(ZVSOJ+hx(YE03=3~j@DIt=jh_z@MSX5c z#>@@qk~CnmSE0iItTmGE!m8ca+-x5nc7AOC+S%UHUmUhK586lCd+KLTbLF!m$j zQFy?4dU(G)LX2770N{d>=+0z(fr))6qB5Pte<(a35a&kL#&1&SjH3=dMvn<5+!W?@;~?9a}__q<6C zQKr3M;4zE8p)%p1)Awulb_(ygiL8%ao(kob9r}to{QzdsH&gZ=GAisK=PCN2w8tph zf0Li@ET{k8a=hN!BK%_}iG$7JqC4>}gK=?@pCqI#%G|k$0iGO60o~4K=V<#)dwrA1 zI2UJ?J@eVK3-;_+_Uwo~dvEgVQ+nd&Ro*staO!|^=cb{JbAA8qkpZx+o5P9Q3q8sz zW*Qn$Xq;;o&1kbhNwN6MCq7RdhY(z;fAGi)JTO25-6t}%OUy$dt`r<1Ip;d2OzP>0 zF%o|;vyN0}C^D?qfTcR>gK58DV6q~NmPEbS0vO>Lt)F@*?kBU}Y}MeMjtEml zj$nGVGK3aXh&yhizRiEk12{gq{f}12r``Xp)~5FVD)ab{&nN$-7(6`MIC^`ye_jH~ z0RnXKc5{2}Yc!LFcjQ#Wh)hw$T>M_fbeM}r2OB?aAEFq-QG{#2i zCS#$PyTz#5;Jb$%+C4P2lPu!({@b0M&O!S>-?k5r)+`ZRN!vp7{l1=*GdQ;=9%e^J zsrd;1+j5#|`sZt4dHCj&(7#=2e`Mo7>W#|W|M%JGUorsth}bY32I%S+1ddsKFq7+) z=V!u=hQ$&S9td{O-f3?fw($U7d-G`T;8!#4$4KQR7shHwC9ULI7uUt6c36}ljYhOg zkF?I63(33W>VUrUdo=3w!vXv&yA{dElj5LqdFVLv*-~K%T>W5q<=XD{tR^+ul1g zGFL&WjBS=(fdAoMh#bWA^>sm}>%;-@@Z4g$Jk8zt`vp6wI4#@t>~72NdO^u{y5)MQ zT<M<3pi*+HCAaA|Y`a@FnU<=QSr#Bse#+eG0c2K_ z_BLTP?iRpYqS=E-dV(sJ%HhORI>U4>Jp(6euK7K{|9}5v{J+$givO)Qs`WYlpY#9O z{QvhXZ_uRxe+gsCih^^n?^Eo2%2?+25+hSoH;ier=2Nlq)D@AN}Vc3L<-@lOh;U$01g6Kcy2M4}>@>m${w_;ze#%3eR{(g}%m%tiXnTF_Zr z0<0K*7>~clGZj2Fs;Ig`y~;d{=;m8bbiN1IBC`VO?Lv+=lb}}!0b`S?S0WHHV;vJI z9JSx26^mVeEh&c!W@sH&`AcCYhaU!E^n8O8#rPGXHM> z4^UEC{+BTVY%c%j`u}YGUr*(ecvv9<+*FgBSQ>vZQP!4VY)atFn7lkALMzIHU$M!% ze5-}xbWgPGC>X7~6AmWtR;Zf3G?*9;URX}w$BRaCA>X14KvyWA)%4{;IZc9;jpBQo ze7<6&ukCp|5LqPf^6z0Va>f*UK=(eZr3Pafsbe8B4*1{Pne-U`Z=H`E2h^{>jsMrm z8UBB7*X?=y&z%21j{kox(DN_I0oj5VSm<(T>JlK&hcsIlh?a_4i#!L{zmZJHo6rYj zLq}Bk@?`|SReCNYa`->d*x%5{6VCtJjoSSD|7Tf$?ZM|;|9ZJrnXmunUVppLEYz%_ zZCC#K`q!(~+4+B_;=dY|dHlyGT7ThQJ@3d&3~0Mln-Wx{en>wXc^- z-In8*8ue<^tHEN|>!47S>a9|#<<-1ewNkQ6&9dz~Wyf#Xb>A<0PP5bux_-$vlY0}A zyOq>VfBq28gMMt`wL1+nIQbRQSEkvZnfkzO08eU-9 zUv46F8}|>(?4LrQ|TY7D*AUT zw$t?8k^_vO()7we|BmgofXbaxBXDaqr_!ob8(rT43!&tds-9#X8r17v ztyb>_wWe*Cs@^Ya#}&FQ4gy1mJNhgnp6L$rvLg!^8emT|CKEL*I}mT z^#A41fBi|cUsrTr>UwpM=OBOl{bm4aJZk!7r&f2$j%R!2O0C?iIF)*-;ka(IQLmJs zLc_M*O1BwQe7`ZL`_D%IXug&HX8O<5|L5@^UlRQj7Cem!P*ueVnoXza)GNLdw90m? z>r|Ru$Ekaj5*P)b6F43Se9x|R>wdFaZ&sULwN`dpUaMTPn-#l0_y2!=4*LHb_WxY} z|I+B+emeap_J6bH)mzni1BkiOw3|+?)U3E(*RR)|pweu0JK5at6mL+d#!Ajx{XFH zsC7M%|CI(P{7uI$xvhV?UvGMTHwfHH-S+C;dKDChIsJcf`Y%^LhyFkJ|9@HZ4_NS2 z`iH8D{_CYm14Mqw?s}E3-w3+4S8cd%)hSo~Mx)j8%Jra(8w5?i-tv5STylKR1>>Mw zv#WDYz~`X%g|NEuUf8*)&-%#}Lcy`SJ z`e#o{_`}t2IFZa51d6=>$Qy)@?aWzfx{B%T>2j z1=(M6x@D(Qa@ztU<_`lM>9T(`ST zAfiUK+-#K_4G$FmdIbyuyHRy~r`4^LN`4tkgpyM&d!B#qdGqr>pM(BepPv3}^ZoxX zjs9Ctr~j6s|E^na0dc!cr{o8d zFd#6nD-Aq@FsJ`dP5;f$aQ?SC*Z;pT`fpaAO#jW~{I6GO`F9?GQ zQ}L_apyYow+*Y^IsykIT@LSzhxf}SkQpxdaP1mheeA}7R|7W28&!GR;=J7vY8vQ?A z|8F+6_@7GMwVPGD(k)jTwR)@Bb;^yJTdCE3r`xT2fdeW4=mB_8xZ0|Bt4V8 zE~pK255Q-j|JLWA|9Sq8FO2?MPpAJ@LjSF1tK@&x0=L;}b?ug4b3F%SzFVrgu2X9@ ze7hQWbs)8%)hY);rBS!56}#H->{d`|wMzb+{yzi#e-8V99{=;D(f`x!|5jrEmrI^g zBA35Ywmqj@YuZk$(XzYEZp-n4isSoUtL*x%M$_w7>fLg;RrBmptJJJHwURU6|Nr## zUoC%sj`+X1{r_drKVZRA=^v^p`mY5((23g$T+gl9uG6ix+-A96X*8RG*9yF*Q?GSh zcvNoI{Z_5)LvgoW56We~<&;|8IsJcH`fq%MKAv>`cOL)u1=0W0LVuE|AE4>7)2x?4 z#}9y<9IxS^eDB(z|GUkm->6lpL8EE|OK^YeZr5!9t=pBl*C2 z6ep?&?o_VZO&e&x0$>GB8J<*K&-QE0idStk%avNS()GHndb8Xu`So(E*{rroE!(rb z0QHpr9_N1>*3hnh4*5T)|1XLD2@9Ua1gNUw1VOD_1p}a7w#$Cmu9m$L%6E|Wo?m}2 zl^q`%Zuwrf>2~X-QneK{TaHupoO-K?rvR(|oD+O{`v0psz&!r%zXSa@o=pCU9?&h9 zYF@2bZ`7+G<^5K*ia7x5)k@3tOHEMugG$hF%MI6qS543eTEGGtb*~PZH)w_Koccct z{eO=A|GEGFOQZkF(-}ar|KIQeuiAfdJ>1r7mhEQKbE>ZI)|^tcR0_OC;0JZLTWVDr zPPgUzbq9KKssWH(({UR0(wqZ)2KxUD=l`p7|Noap|JA3{e^vAUS6YpxTlZbt^BR=- zyIZRGcGdRndbwNg+6}*1_bRnUwcDus=wAz3e#vWkZmV4SOZ)$G`hObzmp^|?{?B>* z@0UgYU>zntX%tTq}zCDl?hC^!64y%BV4^=_?J_8VQV?6d;-;W|)`F5>DcRN5occcp{Z~K7`JZ|I&o7MrtIa1ffNB!|Q}N4{rdRbq#cwt-<9`LS z`Z;c?)-1QWwo`3Z>{c~spxS>QRLhlex8jt7vgeg-7Yu{B{Qn&E{~6+c=J`LqIQnlr zo&J;fpZ}k|w`-E?NYeE7dVU41Z7;MkBN^eJ5xb@f+2}$SGD)VDu4FP=goj5DXW1YJ zWOwhZ?T?p7kwg}%zyazckf%_HL^n~DnWu10)_GockMNJu%Uar-rgeWzDFPzN>s0lC zW+t(4`Wuv)w2b0oEH{8fn`90pn7sMFJ^wrZZ}Y$Ig2VjZ%m2G;?iYqV(aftBaP|A0 za|o(w1pJa)5{O^`XiWvE6L^lsm|^t@ z{x2(l;n=BJ$!36B8gz?z-$oXTU|+{@&lLVY*0A@PGk4Hoo>Lhqd;^niV-|lBAwKp5 z*jdPudieVeE;(oNl809fbmAQ}+nNa);Lq-*hqHiM_x}GG{GYM^zw7^>8~=2i|I7YA zy3X7(&khEO*Sw#eMk|SG!5;sc#nPPKVP=w%y;o1HHL&9p0mA10_WUz!{x|=8-2cBf z0kn=uUc;ZqqEiP@lh#Y+w32`OQ=ngLEVkN^UpE4YmR@K`fibIW{!ifljQ-zy|L?i+ zugCenQ~J@9WbikH$rg&XST{{;Sb|KH7j z+y%$^znA}$s@Wh=C-q4|dkSz!pbs?g39dBQD##1%1FoxNw!0UR5G%ao2(EjJ8^BYw`APRo4vYR*S+WUJ9>Y<-{1E? ze|VnPIM2uP`FO5#HTz>D`<{$!Kk7;cY(%sjfJTARRXAoHEIR;HtJcjY)H@>dOT5{Z zt3L5pv+K@>c|LDQXnZfsfnT^Z=GfM=k`5$^I0{x4_=jQV_0M_Kzz{M3dL%w0YeI&|*UO#fE12OjGHEqk!qWFc6c(@MLXb#t|p>)7ng zsr$qGCzALb-&`P2VT49vY4TK=HysSxx&ae;J8vG7vO~r=HG^I#kPsUTN57>KvJR$# zAy=^2X4Ah{fqhV-b}EFjRztqC_h6O#I6wOD#A=(V_gS+12INk}?nAY7`;a0T%SA~S zO{CKqT27HM+CO=Lm4OaA>*aDW>oNOgLvhg#lRb^l1@Bu^{Xm$E836XkwY~yPi|BwK zu(N~4Pn#g=U0x9#g%_rTRvB8~F39(dYIbfS`@QS&M}-xZ5`XdUz z>N$iS<(X#9&Y(}xxE&>X6Xz3Jxqx}l1$qGVnmp)3w%MVkrO-1=uT{sG*clytp`*@@ zI-9EdenCtT+7e`a=L+4A;CS#W(`_GOgO_C&mzO=}cQ;EqSVn=t)E*YU4N6wU(LwL( zm>4dNLukr!z|QNW%~_lcWyAh74&Dg1(GLmq2RwM{=-20sz4N4TS;OTkhx8;Q9-pPM z@&`X6Q=g^oPNivo=`a-L>KlHzv0Q@1*wj8mp`pqUGU^31G+9KNUNXYgLKA{Y`K@SP zJ1si?k(&Fx-TB_@_7d7@X;c!U@h@&X6v^2S zP%P;Dde3Zy=oHo%a7d}+n!VZEiwlfOTU~l4vLE^*S#*wr2&VI?Q=K0bzF<}0=`-*y z2`qs8H~UVlDEK0Epclm{y2m{!p{)%hGhn#vutJx7~XR?p=nH=;9|fHlA&3qgwQ zz`^|C=kh0)6sT&lue2;#+mgz$0c5NP)J2 z5xmF;k{x~+SsoKYch?0Rm$fi~=&niEw2ZzNm506@i4v{k&t!aRHZx{-Z<0*E)r;T2 zjsjVrKaXMA>P_CEQVcjKHD)QcR`NlIdF5>Jd(H8S9 zn;0xC92@|n=0S04RyybghD4jnC4OqNL)HRRHj+QmDwP8|LlfHh;MdRD-;X%5ZYO_* z7T#FCV@wVFF++d9*>02KluL!Gha&W^0->8MsA15s2Fmv!32ggnw_U=JqXm-b=5#-q6S$qO5!mr4S8JsP{m{?+NSyJeI*5 z0{X#l76}z;on~ZG7;c#9=6sa*f&*l-818IRZgutE`Yj6@E$5`08S?ttg2tZ0mSA_5 zei2$UCB0b5rw8nr~Ol?BCWpO z5dIzEl3C$!yrN&5EZIdf9#aIMkKj=&3B5)J$oeCYLLIEXI|7slI%$z25=;rw8JC5G z-L%fJ#;!U!+l^kiV1JZ*4? zA$?x45$9`=3f+;5DHarXa>LQ?hylHs*s^w>%;|;e!5@v%eTHgbEJ|DlY%`G%g{Xv4 zeIUpRtQ%9~HSV&~{M3pl8vUiGs`O^#K2IE@-Hms_iHBv{P*px`_-h-gfKfwG)lDEisZxJo^fFETc|HHftHu(> z)w)M=m<|uOn0`;IGRhiz^T|5b{=VV3SA9qjOai}@H_3Tbf~a1oYaHN6z=#rKNqGMk zYKu!=fgZ@zd`fy?Kh!mhU!u1Yxlbc){JFn=R#C_S>38Lfu#>qf(N}E@p{*0(CMFWg zzXv>`L^hEi53N+6oAHvr-O{AJj7MK78~5`^dZQlmzl_+&S$JQz-hMGAR$?ArR|`}8 z{?oy@NwL)}XRpp}2?AJ3MIm|UKV~e(qhm#)#Qg9R;@(%jILpd4*k$2vQ z^Pf7l)O>2DYnDsFr%T?O*S|vgZ?xKV=O9{fl#WZ3#e-9?w|S2M{Th%ig;XN^m%-p)fXCJbe5CC5NLUB+kxM9fD2Suq zV}E%^7Vdo&B&!qKB*_S6w%-Pi0K^ir5ABkm zA1WeCMP&l+*<(_5cjvMlZc4dJTNaj@U?ioT^Q)d~6GOZcFZHg1)wa$}@SqQbqej6x z0e4^;fGY`GKMRX%>%0!^KgIWiUE??Vmyo6V8p}E6!jXDfI#y}SnHh6b`cC9Jn~4nT71h+RaKAa5Cc1{wjnfGhvF(@gn72HWj0JD*(Y z*o&3JCvUj8JkQ*%z4xG~pZ((vTE~~7?gv`b@&_HFME1a#0K+O%76L}40>;g3YNdpb zf7npS+bo%qi>HapN_R;6H~@Ehh)-z`_M%}z`Cg?57Whu?O3SM+tQdK{I+yn7*OdHQ>D8N1nau&Jm7(q8eGUuiOg@zuU;Hs6b*Ahgvr`bW$`Qm zejTcI`n>k@+b&aEA^1BAM^lK&Hts{DUfIn#Oo-CLCBu1K&#gUnGDL#)Q+|7aygMMc z4pf%^&Sk#H>uwvqd)mxgv^K7^PYTK?f5Zr4%46AzPdvuBRtPt@-5l0-+*F(HEEtW- z8M|pc2IIy_7&hoG@+!7s)HbH7HzI#%1#Krmbn*mxD8)7NKdjzfcMApa8YM*fTdbqY!yWZgcD%o5SJY zlFc|Bn{Lr)y+8|RDUQHZv)mY+=g0X~C4@@srJmFsJfVhw&G-zLB0vW=FW}MHWi;U{ z+~jWD{Yu_MmL~2)Ii%nEYxc0?QqGW{-Q7RvzLldzchz2^X-{)~LTua!@LfVTFzulO zf$uWl`T*!6x1pK7m#|@}(dq3iN`n={PSSG;=J!2DBW$!{OcB8sSlN#jj2N@aagFU^ zECJ90%S

162f|+YbV)4uVDB1nwuy_onfIKk|=2ZFuo`wHd?3@b`B5wW%&el8v;h z0|GbNo8CMBI=HtmkP|8Eksh?eas**xmV$ZzBv#g~qxdrhzqjNIf=2=`reaa z=pj|l#{Sqtq4p;-;_=eV;%xESi%@qz*X*)=v>OGSrSu}|x&}V~yKRK9iQBsdN<21- z>62%dxt;hoJt^MeG{lr2yldbJ#&_Xe^5S0fTPND@W}ZYN=e=2HDW?YFgdxYVtE#15 z4u5iTht=h?axSaI?thB{`A$D(h9gnTsbHbj1(G>D}^IkvM;}?vRCqtZ<2A$SZ zrq~KtFnc`fV5f+7J$hHv*_K{1EA1 zA0XPkUV_e$G=P_!eDtq{bHbNTchAty(2ztng;KjqPc(gB-a9>F_wcSq7DMPJqTPsv z%LjjdY-4dbpa%B-XKepY@8J-~x;77aW2TAeI-D1vf#p9Y^ezldKU%KW*u5;&zFU-= zp+k+aJN?`wx^M1(hH`#z35qAbCSxbT?imVXK49@$*R@*9)gY|D(aiXi;P6D-wUs|9 zK8ZswyOh1m3hV=>i4pOl{;F(eTBc9wE+5+H6%pFNE~90D={Blz?KV8yCrjo^&+3Jo z1=Dxq4qeXOxaZ#^b0gB{N1t4dpSp*Hzllu5@t0$CCq{eU0-b}X)9DA4(?lNCb=AW! zyq3AGBbMUNY1|!s$Bb)RInXMmY^=)D%EVR7nonBS+BK{99e5ZBdzAn;7)=A_NfGAG+jEKy>R{C$tBk!=OMMq=gv zXSaA^K1rHjgZ!U--lAt`KvG26@SXCs8jIp)$7|K2=D9oEcB&=SIlb>71T>SDi7@`mixD_&}UDB$6J@0w(;y!@fHa!0Y5g<;XxAkbBaAX`^F2vz;1*(7#}J!nXh z?0NfX+15nEu-Kl^_bSG!XqY!_#V8|b*-bGD3P_1?sx_-cA+}A71kGd-(H;PCZd{o_ z|Igl@3Tb_pY(FcEM*zZvJ0+x_Cm-*X9(yXd{a3Z3Y1@pPm}XBIISpXTG>U# z5&www_G|sAiJ^-jhd$Uk>{zGhCY_;HGdMIH_zg6rpv%yJJV}HJz2l6C0XGI7eTc0* z3orvaptm0+_W?8t`U5aPi^*o^Lg?-`WB|7Lu*6X44SX6w!p6&Ge2XaxJ;56l@Uyxp1C- zG0@&>f+6t1zXb8bSkWAU*@NqK=S-ypZ#y0nt#A~Qq1`D?&D?LM(_&OtdBi!N;ks$< zt|jO)A=FJi^?Z4s7zz0vfj7T^%2~un_>fVSw`(tEGqmRjJ@*`IT@gG-B^kl{EN}Q) z%8AhgMMabPeqj!sRCe`~xlyAD=p&;`!Q@$Ui0rYC-NU|y6q`Y9n9JSb%SWX1j!HKQ zvUxRU|B~u44eV%1-$~W9ksLlLcpKxTuAj&+{sdBpf_`n|WWngBek2k5C>Zlm5^r+D zG}5$vBJjBi!wJmvvXB$uMt+YG`&;)2bm*V6b$=XG0$Iu1=Hzu!%)xfI`C;)pHW1q_ zW^9O~Vb@5hOA+f>o^^^Ni~9vurdQXRf-EkHO*|dSy(^HaR8A5gCYKU%`{;DAOiWfM z+vF-cJE^R^p7^-9qW+OaA*@&0$h>A(wJVD`)#sK@wSC+942ze2(+(RYPMcUf)(tM1 zwPD$W7D*FqKe@N=sh{m>6Xq%X1d;u>_`jPl&~dkn&CV@^p5@QEOjfZnc}RKr8~e5e zWPs1e?SO~Rf$pf&Zz%)3K~ZJFNb6?-u^z=@Z8=4t+VLvU#PELNRJv)G^l&53mp_PL zMhn7MDh>n#o`V4Ad>Q^M9as6l0x!Yg<8sBe?4K^Ruw~`2)=?)w{9}wd>M91YX}8Gq z8!VN7z}6ZCv1ki`8=e*}GX6c@c{_PTB<72Nvl#l~!7*qC&@|5JE6Nps2pd1Ri&jJDAMpe0n`j2~NOB?5uj&xk8T~Oe^ zWLoP_^;E}#ema8&^igz z4f1}2ll!|G1#&NS4qILuC_fgNJ=w48!eRKGb3r+;`Be2nlA|9r>uI^z53PC>m&l#*{c7YgOHRZz>cYw=c2(7pxtwxORt7&d z6DGSpkyqkPFvb|vECYlSY(X&yeuhBW3Cofk_3roT$xku+pEA;H{YcaJ&g?MZM>Nsr z3H^QbJkjsAf>_B&ilAl`5&RE#L*Q=2GlQIH<+l5FKVKR=oleQ)t~|s1#E11lCXKvR zsN$K2y;g3Y^S?8~`h{R}A36XJ9s$o1M7{8h;hZj0J7icnV&Ns6Qq$_4ptSG87<*&% zM)`>cR6Bj4-vXZG{1qUgi(ubT;C_G~=1nLc5Y#3tZ%xI9S*91=E}%Q*s3`l2L-Ya- z-=qGUX8HMfsamZk4Q=Q^-Y{4rA&TrnUPgdUzzW`GowX>9_#r-#n;zKKrESpEKl0cn zA95FC@nbG(7h_kC+)4OZh5c`r_7 zg(i5OH1ax@kz2MX{>QKsRQn8u{H!VK3(z)n3=k7YHsqa$LxQlZL(x$h&b-0o6E3yHZ--PO*IWQ>)z^7X((QyexRlmBGPU@n#+FjEYIk&+j3_5%8`5tyz9DN_m%n=_XB5& zXZj?vM@#KC(;?-|B+Vwg!<#^zY|U0II!o*k+Q}%bIH&%_q|-mm zBH*7k{u3MB{7;J$Xf926?^9jz606XF)fzkxt8oOxO=li*K3UYQs`hb6tTsXC`AXpMrNNU0Wdbx~4*m>Avm!6ImyV=pj0%tS^XJK1YmgxeN^cKRoU&=d^3DJQ z@Mbp1u7ddV8z}Y|4REQ zm$%9yfGfS+KlbJn#}PXst?P)d`z;Bs<56tM4L36kq6!Oiv{&gj^ecaWw(q+Dg94j) z45kqTfOT@EjGcv(!)e2h3%ny=48wkrh6X&VICT0TU2%V(eoSc60AHiUL(J&5zWxu8 z=OM0nhnP1Mh|=)M{%Qf|L?uL+w|yo^h!hXlx~r=(HX@&@^Wrs2Jtw%|L_kBsBo=s* z=fSL-w4XR|nn!L)7HDpRMHLvmN(R0MWDH3TK@2d!D)uAICb&JU?o$@(%d&r1AqlQ9 zg{Dy>f%hqSBxD=HWwZc|papvCWGfD~r-fEKyQhul>AI#i`~!j65_ zji26uN+w=z`T0KEpXV=2YbZ33OH}adAX!GY9-`D1@$Ft4QV%|;?4hEd@2`Qb2H*Jy zeGPPFBXhn@N?&H!*T4xyPTw5 zJZRDOs@ifMxiWZ?W@IwZ%Y>TtE@qRA{-#I4T}6l> zrPmtNAWmb&rb;$_dNJ?(Sf;UhPQ#dCky7;V;#K4KE5!~LCcT4So`tH0T@=02XWdK! zdJBMGIRjMw4{mO>8&hQL=e7IA-kAuNJ*+3olDYJ zWIxy$j1)0HlSEAU-bEPcHaT#Rd&j{pQXw{bM)l|IyMQ-Ixyg!w^l=4Rbt6<=mT{?H zSp$pMZUiHZyEhwN{Eq)IN{A#lPr##je8=1-RwkU0Iv2u?QpIIactUyXp(9|CK0RAPB#1f?R(a?cE@>y6h?Ak9=85L<} z4lS?v#HGI(PeIa}2=ErM?(A}RKQ@wFkH+P~n4N<=I=kJgpAYZ|m}{q@gpH*usu^jA z^r&s>l#Rj$KFKhhHJ&ww?&!;VPn3O|pjV@&tHA97Kw82h3G%_15{T(12E+V48CVX1 z&RG)llk5Srp_l8GSozKajQanS~KP+_l$&-j{Tgp-L4Tm54|MVYIBViAi z8Zf1^pxdc)3{vwjyLC>wKFqqT`>km|^hMz3LxS)HS6Qm6n`g_7F#H501*f+J?jyk( zj8*_`Ke3*e^DJ7D^X1EK6 z7-2;9ec|;3P24;V{7S}6oh`~_qp(-A9cTPK)8)Q)sBF%t8*CNdP#3tz!#eNOWHdvc z$kl*;*YYnN@;|->frO;+>wNU&=R?`&UF6FZ4O@7HqP-H!pS~=jp$Z$O`((cGoJJ#6 z+#baA&Op83CjvG!>YpbO`a}q1E>&)pAAZ{{e5q6Fi=MDSSU|o)bwP_y;g zpWUGvW=u`-{QCVB0sD{>kwdsY519Xl)OlwP4)4=)XGd5TB0t}(^`>zY9T*dZitR9x zmpW44bBJlPQO(>C4~44rV05<90q^(POU|1Eot4R1mne{KIj8U4BU$314hFps%1HZ* zDTbH7oMYpPsXTTgT-v%tM{@r7OHv-*YInbZj1vGD&tAl&LEp)cd@K-$Nb#+(QW!Wr z2WVFh+-eLm&M^mYkS`rC9waL_3kW>deW;&*!l7?SC(m5qc}tw(nnQ8ibX4h;efH-^ zGTc+%)MGMmRIw}zsDB+iV`S(YJgopvTVMaoqAyo<%q@6`+T?5Zkj99HKh>ipI_Lt-k)sT)0&f@F0{58TqM4|%rsRjAQvNUF2w>4iCNp-Caa3#PPB3H5c{ z$nQlQm9p;XQ)j;iE8sreg)bi9%qHUBoowzw1{9p-A6yAoo*Rx&j(;e)$({6_5HSrW zcPKIm2NJHa(I1O{w5p!Scl~(i)ZQrrAO%k!g^{B69ySA#>jEvy>vAU)isVtJ1pT0O zr9OSuwZZIyp@3wf#n~l{;iQVNFV*}|U-Y0;H-an(hvWnB&;z(+4y3GND}Nm_nPnIK z^cpp6WnAO?R%^}uWEpYbdp6VC69nZ{h5hzhjjCGW_W%oGa`FFc-+yAss=w|l2){|g z$38I`n6b8$SSrnmn1yU?%M50cIH)?JJ!RH&979-<6asl2Mjco_bj6CV`RZx)&wo#a zDUJ@0pN+ZZp4;%rb*t^YCzHWw8XK+S+0vnrs#s4p3B8T~ux0(vkD+-E5OUS>v1Grs zYfx0I#Bka`8hriK_v&9O%n{Q^yA-2Tzc=TQQlM6E4yszJe}3gZ{)qUMWD>5xzDv$4 z)Vy!&-t1h5Xpe8-RJzWoj=}U__oeN`9TJZeFRD1_?))!>fOv=hQV0v6+vuB%p2eib zPk3e^H2>s?$vL*v6i)7i^M`XuhBE3ft!~sYNBoqY+5l+M9 z(tPgKhgC%FrrJUkG$W<|IQb3xXisnAkB2X|SVo!g#SlmC2{P6mh4l+S*g;HxpAy6S z5%k{y+YexmD}eU#f#J^@2ebtAE>Bn+0sXYfursY8TFg#;r}6dA(HsjivBhwQ=0Pbo zy`qlpFVKl-BhenB^tu>EFmukdM39QBNl{;K~Zt~;akh#tmgTRu>+KMaM zBFWbhYWF;EP`npV9$iRsqVj)wjq7Agxg1gBDfZ&h;N|j0>FVyWkH4BVlyx(TX#TQm zK%DJ&3Ic^EH%jLK=Qb7^I3UMR5R5Sb+X!u1hQ4~el;k-2DNouc>!MeDt-e;P)k7;5 z+l9|NqVyLoMX;GR@?xlt`i(4yq8ia}8;}eoV!XFmBKNR<6bF;7+*;?=8P-mIeJgrb?FrWx;QN2+Jh(Crec5jmyBpXg7kdBJDF$WN zLig-0`)exVrA1UWw)C`o<bq(G$i=HO+5GMsN0*dj?bXx;^wo3Qo(f-U$PD5nVlkkU1RfyCHFOf# zUZ@CGgIPk7AH%&hduTy$$mbx!D=7o|J-~nbfG@w1*s+@lU%qu}nzg;`w(DEp4rQ}D zXR3@A#mM1tniIcrIEH^*gtDLq`@kdkFh&Xp9q@L{E2!!RgGQQUlf7Fs67ImS|skuAFwfr|M=ov$LNpb!SI=Li241zioYpSh?3*83k_hwjuc* z1QQowpu4}v1ySr8!1_An1kwnxg|h}*wG_jk8B>?~Kw@t@xeu_EIB!EQBg-+ALI|MA z1mpde5P{GOrVH0?C{198Qiz2|!3K*>FhT3gCEw=4bwJj~k~d1$b62{@>nN-4xH@PD zr3mL>u#`;73>s{$5;xaNLWo}`v27uN%cG#Y-B-;95QZUt4i6G3(nrDu>ZE4wPbilq<>C=~qF@I8Wi zCky$Gb_l$)_8w=wh{KWyk>oX239sDv!k!JfsKZZQ9K*ONYv{WAN#G?+bOpPUsLwAX zh{hO%8Rld440livacRxw=Vjb0;9679I|H>r_$}}l46)3inFC$|+<1O*i=kS|>%>yd zdgw)#h@|mc<8`@Fsj_n$Lz%3euQSW*gn8)KcdvurPT7r**EhH`Rp!&`Ohq42k+}OcP@Y%@ZQcri>@1tj3bjn zp*pv$Pu;gx>=raT&&wGH>V5sJ@81Qz4JegX%j9gHQnfVD<@nk~bVQocZ@?u5w1Fs) zGSEc89UX@;J;0K~2vtmlo73dpaFmojcB@BB)-|=vtK@uPj+z|(jWSL+xf8P;@Zq$5 z_36(Bs)HBgEyqMW4+B|i@U#Xi1MpkontzfHtzgZ;k?gQBF{{w{`z?ge5QB(z2J6%j z_QVe<&%|h+JwJZbu=1MywOf7M0~2$YcI;FJK)PaNK)^F7xE>4lK*FqZyVj0E#~ln9 zoR3Ow^F4T{C8mF}8egFN%5cXZ!g!fR?}rR<+JcOEh^zo53-Y#s2|XcFv|e^Yi*_pe zR_Bc&%h`g&uZGl#49ZY|P^OHY&SA(_N3YaWjPAF8HPfB_zl;9+0KZ}Lzypa%@nhuV zvwxATC(ABFKiPU+Bnc*gG+fJi{+>U5BS)N^bA6+zBhtK17F`kXGFmjZ?K_V!j*@pY z-uEd`FQk}^(1*MrKH$}7NnXx*r^b6MQ|;`XSysvUAwvTwot4J3NZ|y>hoSG0oK!;1 zsgm;A;?pNXXa0K&G)VoLg=HHnIa_gXknXEXd1x!fsWTKU)~SL*>w$W!&+(sL#LW~T zzfiT5UFEEHit1CBwQY8SYE2Z&QrFpVTGp^9<+W0NSK(<#q%r~Ah>?ZFNc0To{tzmy zn*%+2dh3vV^o54%h4UP0oL!1nPv?vk%`41KT~K<&-yM}A$7Su#Ap7wJ|;2V1}>0;KhQ8|dRfAA?E=R7D$$bEBr> zV}Bg!9DSPO_bCIMsl4awFl2P7N zy#7=kR-yb_-f2enTiykF_b>`1HZ6>*`rn^6m+Y$e5S+uUFKgUz)On|t-j5e}dLsc7 z4v{D%4J#($@sZ@jk5aiafYk>(1jJ-t#sFt~^0xVP0|Tp5*cqJ^GfK%CUeq$QPO1tm$g?xf2FziQRj ziX68FyqmgswqxZP))lea#N_mCEOP01B*`t#vX`uD=(p*dU3?t-FzHkrlr}QRotn~o z2Wn(`exL73z^wVmjh6Qq-F5Y?2^d6?p=~BS)~+#k z`U>!PO#RcWFt<2EQH?4;yIZoz?nMriGM*y2sAXlyXA>!zeVA>i%I}Dc-8Mu6Or@z5 z=D<1(yBPNPE~YA*d9BE+TLTB5a- zcB$EGiy|MKejD7xBZ&{J;v5*D$(B`p7ABm2OO{i`@@1g;*Tcl@G1oTe#2V`1Va@vX z?O%JdWBbiK0MiE5N`r6zWO-+>%5&JdC^C<8l1aX=c{-ew+oce1Zn7jbepxJfaOIj{ z-2APGUrwp@*BOu+pgGwl5w668pbRof2LztnLzIk`D3LChG|>8yW_rvi!6ikpPS`o^ zy4B~1P?cGoR>;eeO5k+|_f%s9V)Z?&53<(1cr?!!u<;o_G&KZn6%G$SEN15pZ5|Q( zy`!MXWD2#u9N;${9J17$wyVA=%4-)IdESjlE(TbxgB)c4z2FN4>oZ}0%&!!1dG7LS zH~o;kw{}_aXsF(M;hTnGCLQZH+TS@Bgpbeo6`rr?T!{5zmt z12*o{|Ngq&D`g>|b$$PR?sdJ}Dxc=YVt*9)_qZ}27kZY@y7#7g(Zugzmiu1sEZ8AH zPz{WKbdbSdSq4<%1n@!Fo9j|Fzr(Pz>r?Ttgo=SD4>!{u@`#N(p0(DR)OB1K8n>M! zV_L!LGWcf)kn|mD1T>_Rusv@y9P`ztH!GjqXC1C#yue>Rbz#i5K%j_&c^0*R_7Atd zOiQXjX1$?(61EojDuAHp0E#3)8@aH!9j)_%^rwK){OCqFEANWtO^Mv5KJAu-H+H>E z2`8c$6?{i3mZAUld=>0If{~;GvgO};6q{2sc0Zf&>kF}kfkC&1Mm2spzDDcHe1bi#pC0n>W04K}()HABY2=6apfNQ$`I+1||j z^f*uT3X!4@f{tF-kM-KPrnigoAdp}Nrec7n6O8X5l!B!k1XPiB9XIuL3z|>Fg}k?k z2>a&wkj-pL>Cc>7BU^Oa&=rk8Pt@K7rJsVjXE6GwcyE-{g*Yz^Fed8iT*&G1JjyM| zOo)!0n=SN!IzPQF5WwdD+EBg{Mhtgud z*cJ~SiZ5Zta_%UVEKa$_KQH%Ti9u=dL>%{!@s7yoa;~Jvy@6^ECKT7*U;-mxF$Gf> z;o)o^n#~jMPm?xO^SNA}2r5!p7Goo>HA4!H=~_P*PFibVl<_|$(YqM<9reyIsaY%rn$cl;DYLUsJ>yo?F%&12IB;F{(4+KQBCm4Tkg>JBl1e<1 zDR|cnDK&zoHIzphlv129<-(C^kP&s}GE1Yji1v5&WCnE|v2HK3VOH@*Ggh?@`U(pA zB_QcpK+60e1=S6Ds%DYUAEwyAB#J9o!;It7;3m|^29HX~dc#5LkgF>h0_2^p^f10I z0Y!xfv~3jKr%H=MSI(C8C>(ZJR4h%nZJ{H$J6-Nt`15o8o9picAZKXX)OQXSOphxT zUw^&7%D{~Sb+jES=z_Sj9{al z_@%$x%1@6Xb39PYhN{I5(!+!YEj}?rK%Xk zkF*v_a8m%X zCgbgwp}l^RP2DGuS#JIPSW5o6_2JeoiDPekI*ota;-+6@J$m$+VwK%`mWYPdXB`F^ zyzC6hqZdqpKx^{s6?l^s_)X)4&Er+?m&PG)9bxq<%?!G!E9%F>}ex} z+vQE-ZFk$>tDM0+=tXi2o8O6f_T<5{@M0q5&Czm9rU*Gd~%`ZpLn7IV}p$a4o69$*9T zfC9m2unkBM8HZ(3{Sz7ty2HsBn{rGDjLb&t?Tus0Fwo{`4AcN^vxOLj9^1e-04AhM z8OHl>Vq(SsxgEw?0@)GvIP6y=5s7yV+#-V;5z~LTxSyYOeWKxDAs(GJ{G3&Q=-W_s z`Uw(;71D(6Zn60Hf`A<|>L}ILEF&N82(`#JZKn*eW3Tye%NI{=8#y-_RFR8Rb{{Br zrWBffHY7k#vW`6}v8te(cm{%8VPqmF(2rKzL9Q(XlDtzJ)1P8YZZKz)2IhM;GgV&G{kcndc}M{QOHNC$$3mb?aQ|XM@B<``=rUZ8G_jTtzP6ILf}#kXlU zF9=`xd*z1cPDf#(DUnodq2~IGO?IPl2IWh^CHH`ac?ckL8@rs~i++MMz643A(Ve6| zW!QLSl)SECC^0XPMWuCtCeez+>GkFJ{y17ykJf`)koY@Dp%tw(1FR_}$9i)vmDmKd zd|oWSpx|(0P@uT&6sz*!d-G_C%4xRN3v|XR-#U?MB?mJ42G+`@ZyRj@&KW*zBaj)E%qXs`^gAR#D#Th5M#TyGr2*@)&ev`_2+dS+I-hYzE zZ#tCqdfJDo(JN`~3m}!4?1aZ4!>7TU;t;A08&r?ABCfYlqY@*NrnWh+sKs|M?cK5- z%t{_PuN~Iz%Ohj!+|$(j)$Ma;O)t)S6PJNOO3#oDxiV%J8(a?s-h&G#$D7HNlB4+C z@Qceq3BObpu6?Ws;BhOwmU^k-ZidB`!o-NZk!~QlS<>5!)XA~pFkzNB%0h%jHTNL- zw4EpEgoUR;#%Wi=YnuA5oa?xtt0A-&q&PTPq)PZ4xDEG@51A4n;xhQkC+7@ z+23pPSVyI3FxaKPL3D7R#@ll}mFj2LN1VpJo0k_1VqI!kUIr~IgXL6Wo9iwRsR5%( z8z2?_O7*)xAGw1-XiUN}SvhqjmYq~-;`oT@lXhr9}zJ> zKBPler6TH|&&puLRU}JXS z1X7csIXbE!Iogezv?`ZfbEH+c1WWwLv&yy_gv|b>0;x2PlB^2mZ_mzksvq%4VrpD~ z<%yB_1!x!4AW4U_dyy=A&=A$FzzaRhJUJpK19HKz9@TS8UE$a-CYaY9)8_2#Vch5J zhx+jf0aP9^<|*Xo2>VhX2qHIWZn2=YSkM@1n(#5JFFLde76C2&zj}4q&gTpSShlDn zsaRT>2>tFiimAvn$@?c9v_nJ+J+koT*;#bRNq}0VzS+p;e$Pqd@@a$L7hS(_UGVXF z$yjdj)?|d~I1^wrZcr0!?zE-};)EB+_K*pZ-Bxt)Iq;VsKruU1PG?LtBbcAW`9`@O z;dp$v$T~(3l8m^UEKPV0D~l|Cp}8Emyevm$yA1`yd;doqItR%70Fh?X$OzHAMOou&&wl=3ZVt258EIYW%B)>kb9YBEhD0P_3xfhe z08%#TA%gZT(6j_@Z=$;;mt-L}Hi@-hdUb923k}}_1QR%wvo_u{IsA>wujgki6=2oI zZJ=2InT(XHYY5=Zw6T}Tds2*4;jqIA!)Zv1%kmad8a$WY( zQ)%r{=*h=&`}`9=BQgQgcQe#f*u-@DCJt6!-P^;#*sF+vI|Ei}VU#r0+gka*OLjRb zxjR`UKbitFU)(=)-89tF&xA55`fTRKue&2dM`zq&|MM{6f3h~{*AOKXXda`|+%AZx z5A83Q2rlT>_~~=8hd7n>$Vv9zFeF^~dN%0zhu_i?toQ9lL2olw35{Hsw-fn4BL_ZG zzi-Qyj%fTHUXfkNajesep^8C$-g$V^FQUj|MQ+zA@z=GQR}NI^z;z4h&sj<*?&CM2 zRzXN2^aX896>{k~=iPT&XKF>G{y(zb!lA0=dmpAnKtPZbI4IH$BBdTIkWjj%q(MYL6eTv@ zA(9eO(jXlY9zvuAqy&_d?&iQbXWotX-p}ve@B0VLp4l^No)z<~wLVFPi%n!E@|}x| zWCb6p6wZXV2^0lkH=Uq91TOyE@q;5!d4NQ90nstQ1}@j*ARQc>k+^OR!RgF6p2@{k zm@9i74-hURCs|E?lS4Og7Gy1}y{m|5py~iVL1@Sf12E)M7?^wlT1Vj`aexE|-_Wzg zo$o>Y0~}aNz+J^rIUa3KVXMIYDylspsse|E4%R<*0yRvl;L+4BR5Oq8LcpA3(7F?) zyMPF}`5S^Ce7cdin>d6kY=QKd6Y5HG2Nu}VnZyK`ZZqHDDu~MHxMILn2US%qh$zJ4 zuZrwH!T-dMTc*csqQS(DDk=&nBp;{Ol8$cKWeMGg+~<2d&~Jb+_EDJsoabyCtoCfk zET8BuYgj~EGLW51b8Lbp;PlL$zP|?cf1+Jt$@)Ng^`p?Y!7eqP=oI@Z0{buCHoR1+ zzP&nCJ&chb(qS-wA|W>~IL!*cWjdx{BRu(7EUJbhnyPE)GcEVr)yCGXr@bpQ@7^s* zA|LJxxUeOk*bMpJtPq$kN5eXFdj$@1F2GXWE{JZQk_6ye#{0qwZHZJm?{a#Pk?%e& zU5{L4EcU5><*+IeoDF?ZWtY@Qf7kY_7!nQg5HY>S!x_1eYIQ543NY|}Bu0Z?h z)^Yc{$NH{&@`c|+rQIETyIgIuJ<>`x^f}-qheJPh0hrsSf){|48L(F3unTXj%-fgx zUKmBst*YV*nwm3>4|Q0h0N_J!j^QLrA?Pm7BnrGB@>Ttu07XE}J6;&>NjN^XdjgyQketF`C;FDc zj?K<^>q23hlMUzvGi5w4aGwdc9R+!8&xKK);HDamaaHXJo1a1M2X+togenqi95Sr( zJJ+&QyldykK? z18G!;V5oc!2p+XVp?&W7d+N4ZgUM{3+Z4j)!00!&PUwj}`*fepX zV4TJN5bbaM&`J6(z&<${@b3siHPbNRX^I2vgkZYsG2ML_`?Mv`180__x#A_!pvi*R zYAdHUJKw83t9}B<3LynCV$d=n`K+2bXF>WC&dQm+EZhDCV1QXYcQmyKpkJKeI0Um=k6G=* zs48?@puF$q90_WACRsix(unYMde;uODHWFxZhO+EwAIR>qxTUef^0j&%RU6P3b=Jz zbj?e2O*dwWr{A#U(S;B9veZREo4S3lw?N9DB*o#25IBi zO%C{-3l^dhm>UL6*3KSXM8j8nz8<+@jqBx;-q&vZQtLcm7diVhuQxP`(Zp~DWO$7Qf31Q;%QmT(o9lv-*PFc^)< z%%+gGiQGX(s9-B_x$|GjiN+0x$g2d{VM@jDQ3?E#pA1gsP+pHQC7WNtu@o?AN;Mo% zA)v1u9~CjC(Fi3V(CS}_Z&2_`8)xVFktHd}vL*^=Aj|Kpr z;1XmA#_TGkbJ5%-#L7l$S&`@OQRgV@oLptGjz1glI>C&gq7yUS46T@k z!)W(#j%C0a-mSL~`YvwX2q%03)wLA#W=~up{_-?p(U-}5$C~fJw1?1J3zm5M+>?f_tdoeLIKZ;IJ#mT~U*qcLid-WomfRei_yx0%% z8piiCZZtk%oHQ+k%$F5fGrx9e$;T3e$>>K`8fGZQ+(50rZTl8FF+uN@3SN-o^4HKX z{d5^q<8p+G6)eYV{-tnv${Ii0_d31Rk3*p!Do(y!N7=7*_$T>Zsy)T0_k8Gf7UP$> z0AO(P1Q_9-#p+X*;!S{@?B?!libAe)OSpx&P@}$~Ag`UNZS`%K!J7=f03!2`QA0TS zXwjR4ILu34@mBf=sSl{2%ns9`@zELc_g42(?X={1DP!kv|M0-yo_g8}>vPz1{f0_y zMco*${(xnSICD`|K*U|Gmr6y-`sEYX_~@zgL(6r}zrQ|h4D(i?1RH3uw}C+U01adc zn1J?S``!XKj(91gB-OjdE-c|Yyb?5!#hGVaM$37y&&Dt*z2Vr9oX_#03yG5r59R}w`4thMpx zH3;1j*^=LI()ujOD5TyRRJg!lwL~CJWINtdls+4tV`u97Q2EN<2F%~zQvIXDiBAc- z!o|v0@8GC?3MyDiWxr|}pSQdJ)SmLUt?}y|BAUw<8R~v^0yk2GUiO_T$Qv+)4W8~f zJ)GanIm~L;0=?&<>Tktyf75(s^{lTvz3=nwliL?P%Sw7c^QG(J0sH;EOB?7#7%N9% z?vw6xTHh@AUU%h_5IbAd-F#PO+ELL9GLqAoH^_a>cBPsq3@pZxI*V0me0!V%&kix8 z@W91C15dO2vR%*eTA}+a_2r~hyqLqpM&V*s!{HUNM(;n}wCpj-$8QdzFM8^u9(u0M z9XEff++g7#!*s(>o(FjJH7c9AVXt>PeCl#8U)`X-^@GD8bzQ3ZXWdJy>;?*>-T7NX zqlqv3pn6yxgU!={H5EswHGGSAA-E<1{MeDQab#&VS8?3?VbosI07~u5L8m#R)A*%l&lA1z^b`~J^3sv;52{S1TDC_+Vce!wZHZapFDxO}%@+PLsza+wlJQo+0j+JoL=0t(7W zehvyB`+fa#Rq}^>%zq()Gc8Q9{evXdg1|@zG=(g(7qKV>2)5s%MG-P_$ zMnAd=s){9l(|RezKz#{^cKkmp6-HK3NaA<7dz3-nk`PI6SfaSD5%2CVXVnU8r&F(9 znbokVoXHe6#i=^Bz#%IKT;HOJDlB;T{q}9fog+@hA#ACT`z?f7phlEq%5fz#XQXCN z@;fzaXETA7TTTEo@!$ydA(O!_x+dnRMxh>R)hp}LlMb|VEmg?A-O#nydE=SX$cX1O zWU+Glm1EyLvn%COuR{#9vjf`q!8jTgyyrMo101Rd7Zfnx=wOpwX~494cUSa7yt@2) zY#OV-4nr$%YDYn!ZF~Qc+C1?dXqngp31Q&!_AmbyfwzDGfrj>_rb zLJ<`EAB+WK2>iH6;p|U>Q(nv1p8cEjUIv&VgSzSUsRIY1(*?`0)5!Ba1z=d`cuz z-QQ_e9@4c|d^IWcxaPtl#HqnXvg_P;DNd0Bi$ni4CGMU7yR*wXNTVcpT!jCj=*|WT2v2?i^;iftU$>yQfC_d3S4A$loNRaEy>j3+4)F^j_Sjb4qXJBe;p^d0*CA|KoAmZ)4ju4 z#I)*aO6IF~q2o2Xd|E&eD|W44gS3l>nG}7u_S?ukr#xKQ))?le=N162mGNH`2hw5%jmza-*Sb^}{r& zI&8vC&;0=d3R#Fm@jT@n5C1o`TpO3QND2d`-_EWKSyI*mm_qnxN?5)9nX=&sO4cTc z4;H|eB4eF^_{#B|23slLw5}FUJTh1;+s`G|q{nL(iQ6VV0#D&H`Ahteogj<>lssou zPClcQU~D%k#(lss_tKSek1Q^-5@LqbELZahM`X=5&pQEJnyNAGL<($vgCP@UNqj#= zgD4#dmQuUna%Yv{iKI!9&Zkd@7k*obJg}Mx#~E=e7ux6x76oz`dxM12S+@Z_rW*kh zV=<`b+kmU5p*HwxG6j2fREKkuSux8aTI+%#LkDf@#&sh4Qk*T zWPd@4>JIo0GcL7qRh|gyG|9vtTb~h_ZvJRA`sBTD{{T@KBEZ4=DfN)@7ynma7kWL8(c3$ZZ{L*D+GlC$JQL>(FjHUu zlG|N0d@8C8Fl%y&8KPwMQ1Y&k??kDPCX$kgTx7Htu1jC)5%$*}%OATN|N5_Cr`f|W zJii4ALO7FX7Tvv@#Zff;Gu-Ca>j>tjxr%%%u6ERz8fgSRa7tZWBIti!`5K&nrE9pU z_5aCi-4P^SKWgA^`$b_ijn_fzj$%1oEws|3aQJpAv!~2LMt}4sFL^)~2m4Pr+OZ8b z_F)$ZzSX}0r{{B=^`!`@@19Y4Sv})FQuU9Yn9p2|m<(-)WA@K{Y?k9kXYUgSZ$JR? z3;xMdRsGLur#SgFy;#~qKD#^%JkBP}=3ZjU*8O&iPWCmv+HdzVXm7i9V7kZj!XXkI zb`Ao>BKU=d9W&r}0;wk9P(3&o5*On*i|aqU!Xk@K&Lo@<>p6OMbiL5sGLn|V%~Fg`=qg3owMFL zTtFs@>k>5bwaFPQg6;nZpvN|_YU+y$*1BE0Z=6XfE#Q4!#>(i8pY7V0u&-*J*5a(B z0eVeezHJd4p+f#bPTgr3QiM$~sD|tOuY3vAx@g2S%rAxO(G947aJ|>h_iJ~|Y=Lc3 zoG^GP;}C`G`&(}R*nc}APNgu&-`HRdzhLj?dHn{gH^nZpeV#^I7@UY)#@|Shh&uFjuh{`o;X**xU2S>3Tm)z-rQP0Kwf=$ap!CT+D~63L2h`AX2`8yVxrK)NiH_YnmBg+r$> zq==h(y$!x4#=(t%%KYo^=9eaEQo24HW^@^v31UrWQ3xP?z$h-CC9%_2bg~K5|D}0< zgJKxvhvLcDWpZ7(^IYS9H-^4^=kM??R;u!rb%u8jk-V!G$My@4R{)*_-lTuDAOsv6 zkDyl35u1wi=Ye`79*G1=O?pF>{g+!=ln2?3s!CP&9uN4rHG1Pr_Wz&c{k7D~$uACC z_>Y+zXNV32wxV7j3i|!phZHv&CpxY)S6OdQdG5WNE&ci*y<0@=-A&dM%5_dYJbd?U zG=#c$i$h7Uqm=5tf^z!%hC5Y{8zbhtsW0%9M&>kofmLuiYa{L-)v!McjEpYvzv|;d zCA1P3#TxXJb0ioyMCd2=e!60giqxR~fHvU0;KCTeAOilQ?oa=dHE7hJe0nn`)SFLN zTQCG!RsgZOcvje$Fh>wBma)mDhj}MW*TSmzuSrpV>!W>jU^1-3K(T9ddkm(fzEDviVR=7CxNu+$9C_HEZl+JA`mk+zI~$KRzHlj2uxvN9p3|Wrb(c>FMQ` zGv4DV%_uEpy@FJ@|D0{eO6kiU%ln@VCw=g|Tz_C;8+JQj#EuIC2~T(h9pCMcGwo^9 zx-8K;^W;5f{9>VZg~F@>UycXZEru8%YVaR1IvuF>#=~dcRG#@Xl#s^u-SIR*Itqwl-2Xq$LMMy~QiD6}n$w6S|fFESEx?0QKGi4_cc z1R7?SN@rgPPK32~+<%~Geip2!+{00)l|sS&g8V>+i$jk7P+KRwstCylfr-O4)* zR$Sx-Hh&wcC4ao^GmD^zxT%}WloBuBH4%*p<=_=IzssL;S)^x$(b!-}%x>6}*R!}w zGf-31ejkTLI{)jYfs#iURHGF=lK4w}{3kxUL;24lVV+RG0Hw})Xydibd8xfHl}OFz zEh(||bDsFCkmTQ%%|8cvXtNwLR5dbE|M8*He&Xkmrwy-7Dtij6CnF#D^7LlkH4e>r z@Jjm?{vSKze=1lPHW*TFW*ysP-SH+BWk_Q zE}huQs4ocBpD@oybG^`_8Jdyt2`pq`~AZ- z&Z!cpiydx#$w6v^e?Gb_xFJ2iFxCxSIbF#G6-{iU`0=F)EMOx<_h zI^@1D`+TTjd!Pc5XC6&U$Om%jy zpE4x^MQtmodEZflOOsjrJY*K*cpN{R^+U}Y*2B905y3)OB_6MlBxd8x4q8QhHE8ap zCRg5cC)cVRI&1BCuYrtGrVb($rae;+j!pi9g2Q1z61iZ&T5Bb*D#gNGG`EE)?b!|z zN_bb2aL+y@>)e7!){7q`nf9N+KCoXfx#uun>0E|gv1oSVzTNkkp?j`m3H$AAgPTIWFm_xEB-5C3+_9g#} zol!a28wyZMgEKb`(uX4&|8dxL5opM8VVhifB$h=*=Sblq9`V9l9x^X{d|~LzRMKzK zRKw4bC`p#Yv8Mm%;wzv8CYqws8%NGt2KEmOl?a+Ihjp62w!^Qc_w`=OKbw8|-0TZ2 z(kk5qH+(dB@RyGU$M0a>1}oxyIL;n#HpFGoh~lf`73ybxM{jZCjq7JNYy2VNSzp)k z$GduX$|JDgkNyXJt8OHLuuH`CWW#J>Q1B&9ua*H@p1W5!wGGdk8PMI=!QUObLoe<# z1t%Py?k!w%7LLCF0mA+^{0q}~RlGV(^HSS2M`U-xK6z-x)G5X26^psvajft^nq_es ztUe~!1E<^b_c-_`;eVtll9p;wlIwLa816W}dCzn>&zqM(u>A&PNP>|**&-=_PIJ$( z;unMkokvg#c)18_rUBIkxN#PcW5w+huB7Tj3L#RY0rd)>))Fv?g3N(4%>{v zY6(%x7y#L839TeaIxg(D83dS@q#;wzPz0J*ipkm#m)?EzVra;hp%nD}iD&$S@~3|} z$Wtt6*hn}uR#@jsHC zEQT+NtS&i?t}3Tx>8AP629CVf7~m7Upzh63ihKD7Zm0f^y}17d+86OZF!%z2#2&(kM~C%9eXn}h%(8ub%s z=MwlC_Btu@=EYse_^3jpHB>r6)A(&6%PxAZ)Vt+pS*IP*%S|vZJPp4{9K*)vpBD*+ zLlYPL+i|VShFONvftRH!o9xzpl5*2`=_W-aqPZ-!NmpH5xuDl)G4R2dyo5{noCaTL z@MMPM*f6H)|?iu zxCyK|@uI}=?Btkb!UVH7BzQX1gDyHpeDGY>qvn|6%%}OxwJj6gRCt_tYEgv%)YH=t zIUN7pz5-{<0b{s}bB$}t5{HJ)`p8&s(vN9v(NdccQtfDtRY+LuO;pbGjToPUKkoKF zLzl1MgmGX+;uBKr^~~FMp4*{>^7k!to~cmHYJ6}@yzr#z6%un>x@C@~Kj~h}AK-&y zxlds%NrPj#>R^-NpkE(~(A{KR9hj~#GvlptPYA&y9yU{SWLXo_3$D`b^3nD^b-1yp z!_&G29=KISj0pxXPG(i}EC_|iXcE1)!rZuBKXgs$rkVLo7P6~aR3%M3AKtq9ro&-w zSnq+7|3!#9#{sW){i^hq?D$1)X7AoAK_8T(|m3MPHkHS>&`=`MB5 zpAC9>2=|>ef%_tb0N5m#CdQ-P93hDx)^@W zECPpKs9^0m@ptGM4YuG6iq=n=EJk?t?_{#8>CVa1MeB}JgTat%woB@EAa0{tBt1zE#@UVq9w+HCMq-d^}|Sx?qtt`36rlOUZT6PCNj ztg1xF;1ixoI6NWzB&1b+dQW7)Itvb-Fakf@+5Y`={zIsBf3$&8IP1*sQaJ_LpFWS( zJp>Q5f^zC)H1fA;cK*H8HRQ1kwoj#t(XKVfr|Lc8VjlhGK>9aZz5c*?sYe{c__Yh^ zU0uy39t&{9CJQE|{WT%?7$mfjmO-iCNxPq zt-_<{QA>@Z^^2&X127jAa5pMry6*B4n+O7m`Tj@iI~GB?qH4lj=h{VE{p3R{QW zz3xgAo|QXOeJZ$7zQbOa%0#~(b|6jPVZv<`2IMz)(sUzXCS(`T-B5kITawoP(s6Eo z3MLsZG$zlSS8|;E{2EGyro&2%8;|+)k0(Zv)Vr-Uii6>OC-z`<6pHFwEQCA`dkb$) zd7xincHHLMk$bd{!E5yDV=tJgtUKk2RzpOskD%O`Z@ovLj7n9^(#Tvo3d6vXyIJ)f@QU*pZZc&jo0Sj0xoRSp zr=KY)&^1IWP9J)gzjFc;^SC}{6}=8lU}-KGV`G$~fAHlON$#&+)3r2iUS z^6@bj>T*NYmeC&0oaJb9$8~hp8Ig8fV94&lek6T zjSL2A)!qLZ2Wxk$IB-Iftx#=+IO6*=$pcHTf-c*T!7hsIR$DU^q_T`=NoFw4(m40#iNzg8kpFL zd4rGHdY^_n`P9CM4DC>j*)YMyQhT6xqh4b+z;`gYzv~!Dwf73VfcN)(TFcFnjaMh( z9Pf2?IQ29Os-<7)K*`3D>xTtu?(Mhl`%NU7(K)bj{k$JhN3kr7#I;H90J!+X9<)CS zj*jc+#5~|A%g(6o)K@o{SpVwXjMv9+9!Ke_#%#>hIj?ON?y>*4B@t9&m;EVN{~cjj zNqM|Q-0ycGnm$?jqWc2j!Ju;@Lapq9`Mi_1r z3kfTL;tYx|5!kxB{AR(<-IVdC9e4VGuFZpX%0k%+f_M%_OHY%1J$Pu zD>l7u7IfbL75ku3c9tc;{YV{^38*&FuY3vH?EBW}5}q^V5qFxg%8HCxo?Cuz^3Wkm z0P&bzo$uNu<_fvoRXB85@~!?De4Fq5!j`MSpC#rVBBA(LVnKVomPo2iN*lSF84aOv z1y^k=^XjBba(8|PIECkjV@p+aCb zy3Dj#(j%}2jLCp-PP(QGtLgN?s=qc zWX(jYwj5GeHV^kjh>>K!9s~Cg8-?3YNhxkmGd!)$-9y|#AH*9~J)5}f=$w+3P<|65 z^(yO~L?H`{%=5@Oy-9Tg5w;wSM?dM2YxSsjoC((MdIlI;+Y&L<6e{4k5)crr7xv2U zBg?ZV1?>$M%6q{O{eG?375noDosHAt{ZKM)P<$CS(;6;db7L+3F# zR&y%WzjQkZ(qH8q{Y1!=I%z2&?5#d3vtK3de*Cbw;r*ZK;s8QWg6y82OXo3H$wS$- zlou?rh@doD1{sF#`%-Q_6p>XYiRML0O`lI&VmLy>kQWzr3YV6U2|V7;ZVUf>rn(F< zigQTCWf5*BQ4Tg-K*Y&&)qCDe4jTPMElaTT)=czA-AGwr(dsf7F8Qj7Mp{p2joT;V zt6cNtSFyWiaeuHwIl2I8b}u&Xl7TL-_)d9|45IFh`4HiGwb+xQ_wD^p?zH@zExX82@&X?jRoQR6c1iQ!_ww&r{t=jq`@sCQ9JT-l z^#fwDw6SBj+YR_Y+?P8loAwCPHn8BdL0~OYA82r+`lHzWPCOAi2qun=1d!NU;NAD+ zx9NG?Cr$hUp-T(+O+s_6yh0CTbXu*N3_2Q%tQxrXmh9|DvfHv^pPnmKYdyc9U7x+i zzws`u7@$^Xm|SgV}(oqh-LWOia(mFUh=^V zwCOi%<5s}dY<~42^Dl!le_Cij32=dj@4yq+4gcbc$fc@W$C}(d$p3qhWr*@NGe-^GCWd9YPze+p ze!jRCqbz2PG_Ru2esy+B#w~MhxnB0=!4(!un_s6orDhq0#eMEW>4R-TfP4!2eyyUg z&en5NMQ#ukaxUcJI`0@V>3SvSt?%7ssa5BH*m+vnrM>eCslE_Wf6Gbku59Jg2i=4WRyo1rZt!|^Tcrd zaBglN@m?Fege%LBLQ=P|(|7j$a`#a}46MOUDN}mT`quStDo=B*LhZB9le_htX=DgC zAn-A?ev)`AqtELc7Bx!;3y-G_y>!IlU6G;a^HiOB8Hsil&;2JKk)OOcn>!=bpk#ePwjydVmdYN;j!;W|Z5HBH9-(bH7jS1gK z1+AfXM%>#1jT5ZuOzQSJD;O-lWV5dNXH=Me#k)a~;W^JxL0o0OhiO=$b6ZIfhAIoR zOe1+{&AU?aPd4mJt!vGvjSV9Nd@Um1MQdC)pQbVRUNllJ@z<|56)piyG@QTcdHxs> zzt4b7reh>0Vk+das=r~Sdb+;emTkBZS9`0+`3mI-VVTaeW`$4HJJ$yNH zkEsxcFFmwys6)8_FgA!*i|T#t!29dFVN8oMl@t{;dO4*{izv&6YJ z#rgi?uBBGvPw?ktx}YlltsxAOuHamraN;AfI1(97u6D7MtPj;u0?-Sq+OK4;g%xZQ zS-OrgBwt1_*rWT{_MHY?A6%bCb`&5sf>kw92(PcSlH)NrRZfE2B*n!8dLMR6-JMw} zEOplP3HPp*v3NecF2f>z2YqjZfS*2Ya39pcq9hHL3@+V4gvG>PW6kVk9O-7%U-j`qx1k4ca`%w*VGGNkf?EbJBX_p#r zyhUatyC6eWJaF*=W5BkW^rDxPTmr-9_+@Ri%RS4?$50#pmOpMZo{swA;T)d|&7(E3 z3s${S)Yr3np6XFK#zv3v^;^b%=Wdoe_#?T-AZ-}v!(RF>ZXcnlhQz30y_&(he2D!c z8Rsxc`s_r{Mb5ZVKRqV3#o4co=`7-GG-9hC<{o_+!P76v0J;qXs;(!U9wu4_k@>C* z@ARQ=0f=H@sY*(pH_IZHv{#a3^{($Vi)ZWQBR`K(2t*0!TLIDQaLyYxmv$)XrWVR% z3qT#$7xpi{d8sqdN$)IX5O)8h$1ybc3Ih8fS2M#va$7{Aa&8lW zeTfOhB?PW}V!6k0X|MpHpwWg2Tus|k)7n)+~zSylz@+rO+K+{W-k-)z0^6zp*J;oRM)=s6UtSt@BfcCF-P1HeosAS|MK{|H}eCd-008+?pC6 z;pYyJQ8UAR`4*b^aB5_AX@uzP+$_}<>go-Nx93TZEb{l;rb?H4LGkvs|F>Z9w`;}o%a`vGAB1Plt!3}p30XBh}&6YZK&4Mn=Zys+F zg>SUG4Q4c3q&_b5_ty>Te^l#5h(PZ~npC!#hTJu6YI$`)cmq_utl_hz#j83en{j`8$HM&^Ds?gvq=%1%tU zcYJ*0_`|eugv|b;(P-@1-i$#y-lnSF1S^*%rihQcD|2gA4z3Rv?<uvLseAbdbC#q$oX5)*X+l(#EC|T-LhEEr&FA`>rCg#4%+w;u~!83KE&{Q z*pGRNxB7G?nN(q~}E9H94yj7I>ne99*rVk91%mnL>_-Xn> z23Ezo<1RY_=USZjD|St9Kdy%rj0&gf~Ju2}wurPjO}M%4@unOdL^8xVI`wKlx&f zO}6s7d`A*3Hayr^;SKYEplH9t6;AVg!v*E)N5vPCm9srQYlwC!Yx~97wDT`0R8pkj z4-831DE#jHI1Kr;T+uipbov;hGH^aAn=t+3u=fL>A1Phtmu|aSMCJZ|A2}-_eSgW; zPeu^hSs0RW84u2PI{%ob`bL?W44d^Ek0-BKjl$Ix(hKI2DRLuiEAMZj`P|~-K1}O) zMNUg<@HIIRg?KS|3dk!hR9NEat42ovu7i*q>plPA-wDI_cIAz{1iRu@sM6Jc+l+^5SeaU z&6P>%iV4xvh|@DWH)tSan-Z|lzrSX@VHjj8y4ATwR_NUNJn@&+;rq1;u3&@^Y+SrB zYutOH^Pc6EXij(Efs2{vd-vIEjT<&O%uBSMjdO7=v(J`u`2=N$X zI-o{sk9j<%)Hx57G-KCV_oo^n$Rn7+r*B^O%c5udRpifKZp0~}%v|zzwvj_l_XQ({ ztPqfzMi*7w~DgTwa7mkc=}z`NRj7y&|t z;Rpd;exP*@6CX0e*H;)wY~>y45_`p6b9->CKHIg_6EU@7pmm~SNh>CDWMKL~F z=s?K-nKV)1)EcE>PUvO_H)W8ALUKPNXKBFw9t}AcQoHH#s*3@&*+iaiztUtBeGz!A z@^btZx8M9+|60IHreMbC9kR-Yeh7u-%Y&6Y0_>&d8+!VF-wD-t%VfY8)^$?JC-~U+ zp4XQ*rp3B0Ns7|k-$?zXu9?Ix+-?aRbPIvHg_u7b7>b;~p>-skK3(R;DS7j@8zIl%B$Fa|kNAzXIP6Q`-gEeNX zAWhfU?{X?H-+aKE`&cAFvexmI+!eq0(tw1(N(q00zV>5f3uo)|=@&|kZW`Y1CT_72 zA##!qT9$6kf%CPJEHjSH442A&{;DrOcG|uBa7D#}@{4tsTPS7659_9AiLGn)l!=p* zX6U5^ex=IDA99&@zq_8lV^`AmJCsJujg}O`|7npY<)bU|xQM%cI+a4j|67Jy4$2hQ zJxU?$C)!xa=%|u;CJD{ug&-D73U~+vl`JkX-fHM@LLl)OnkcoEj8k6ejxsIbfRnXQe>a#oCLcy zH|N_14#(Sdk3IS?^sV*v34gat+tc=MWl<0w8(_Hojrjl{FH(j!H)TMLx8&y4>~QW@m`Km_Dq{qx9d}20%avjy?bMkXQ5mg zTn9Hv~t3gXIE);FJh|gf?nYuMT_fdk> zj__i^+nvYl)g~%sm(*Bay~%z$YS;0FKu17TWkt_va{c~Ts_7^3wZmwpFR$NRu2L|Z z)6@Lrufm_xZa;MJk}c3EDtrX_mHuStR{pHk)W)jY`&-lVBx5}`KO$>KC&^e{jz}h; zu7HE!$Rz6!8%N8`%>4%_8btl!`?nG^w{7((X%uv|O1Vyij(y8H*qHi5ZniYB+Zd1% zH)wRq(vrm74r0L1OK>Dk&r{E`_q42AoB4_jT1BjjF;m-MY82t{o-R^0P*)@h`q?evW%^5}-kzn-l`&q{$|U?`K*uBZYy)pW zeD0&HulMcHM_7npp7*) zx2`ujZ!0hKPyDrrKm&EWaw*aH%|5C9Olm9C#<+=a&w?+Ps zN%3rd=ixYM^(sp5xy7I&UqRl~%P{UNOtCKvs(7Wus&^ zs>{umd8=5dR$mkKK`$>Jl+egXII0koq=#v7x=VHs`K^G~^bAq%W3S8vy(oTv=KFHC zT)L_$PZF#?KaBB@CUEoSm_s+Tcf077Bt);rho%=8Y(*TWMrE(Wy&(S^+6#&UrPS*o zK@sv1+Go0ZF3px&&$Nduq3VNk7M1d(mN>C1*IpSZ3~zAJ3C-+g?m20&Ww}kmX_+l% zwaKifhxgU6*qDhe)J$PDm6~{eHHDzpLgz`EvmnO^CDEL&<8+-aD%x!*1nA>VE4pU? z(Cb{(Zt!w3BZpYG+x1|GbveWYu%B;%OSjoj0PDk>H(z7rRkkp)hl?YOH8sE9B3l4y zOIEb_K3giYg-07J@Mb%#vdk8CAFP-4@=9epZ{My{oe?${yDt;o>Lt8-y>riHR0YkH~+q;IBraM*ldzD?Jj%DN=3YGayriQ zlrQld*U^v>ats{1HEepHMsBx3b@>%PJGK~Egkx? z^@J>1hqbidf_*8Q*uL0zZV-$i#m9=Ik&GA%u&j}8SLU+adE`Gu3pXt@T%a$AT2OJ7K@=&$ z<>iUtj6ng%_Z)8{cU5)%Wc^p8ZjJAJigqqJcY#v@U4xl^%xd9a^nf0UpT&+WLmgBI z(aGZuPb4lAsS_YoJE_*40stk;*{ckD?39wVjwbzug$VJpo2<0B7@h*;0CUjg@N{Pn z(3(iHy^iUBL`f|P`r)fG>bX>S?6G?M7Q!`9asj02eECwa5ziHFdwQIeqc{_y7Va1n z;gFoUCnE+W`+;NGj^&+P79L6RrBjc)uY^sihlF{hB97Gf6q6oGa(yOR2n)!I@K} zV*CyB(Hs56_pCL&q?>M?Jk2I^zEU031o?#>vrq1jWrEOWtqrknaE~2U#7oH^mm#5uF&5K{jJj9Tl9Ao z{;p8j6)L+zWml-|3YA@eK-h4QPkhGhi}<<&3v>sII& z&<^akLB>1c)RmF`4RK(-;DV#_9;D;UaT+i6B_QEt|82#8tN3r#D0%XVM$@cF%9rDb zrSsBuh{FWvs3w4M1bD(juTIfF*$H)qsibugvkdoVlTI(k?Ab>o2@ElCivhckb4;&) zvEsl)OTi2U=ZSI>`%en5KAk`(v|!XOUs5hhi51oVv`zGTe98BJmt*$-#Zq;y|9NZd z|J6Ic6WQsw_RwjXBUW9-3@5bG8y3bN_U{F^EO*c~PF!Zv-N7DAJu;a%O|Y*!w~OoX zZnEyOX-!8)T3+B-YD|n(y#}3Jlj?PUBFO96b~nVC!?slq_V%9C?>*ewd`$jIx~9>u zi+JG+#PCh%QLkt8TW%e4#Bu%z^D%cu!M3UA0i(*+rcnuImG!bc}}C zaEv~P2R#FJzs@8B%wt8e$FOOC^L^?nX%m~NQK!^`6B(#s7c|*r-vDjWvupqXLTkcy z6ilgDguW)@5Fs)KgHWqFMe!auR_zg1kUqQMsj3qKrP;WcdAxPmj1sEe& zxM>36)i$(Q{7}N0mnnpc%yB&y*YhLV%a#B6%HS5>nXUXk!u~J*URtStmY3dPD|7k( z_2FL?4K5r1l?D8203!E?7mp%X0n0ps6|4XH6bpEeY=JsE&Xa3LU=6wD~ z=YQxr_64l}OQq=iuP)7h*ZLa7>bhpe@_#it|Cg3abN_#D==>k+r)oaG-^BT^9u3cW z|G!j>&Ht78{C_j&|9pDC9rHg4x94nQ-27iENBn=3OU3G3{=bxe=gC{foc%&B2%5^2s;9{;R)n^8dd7fBx2ADnI+t zH-F;Se)He_)a~E>?H~NyALRbk@BY1?{k8x8o$cTMjjw)G|GWDC{-@tL{^kGt&wu~# z|Be6q{9pdHfBRcM`q$aJt)Hy@vmamo zBy8Agv5w(V(75P5S3hs)Y5`UliZ$m){POr+JinN4LvMJ#ALcIDJlQ0|9G8(sk(R`Ge&t;sRq-spnoE{?&D$o;d z-OM0`J5@PLC4ivrBoc^pe^Md{SuLi5J&fm|BYK;0w9FXEJ*GSDF?l4?If(tp?4fuc zqlP3R)6JjPvHlAJnyCZ8aqItbCFcLHI*1Opkn{j38JFwMvF_;jzxo=M68wuW zF0+ZusIx}k_dp%HzghwoV}SQBa)xQdPac8Pj0#r(>#IX_49#NohmJY$=UUzO^E;}W zBO+oC`;G}lAa%0yKA!Y4MiPbA$E!x62RUk(k7BCNk8{~4TeYs$|=KSQNvi{WPr2Q72!dX zqaM8ez+!TLndQtxLqDspGw{&pI8Eb!q53V4+vJ0vO_v44j%&9p@>h~w>_dZ$wXmj)qE!l0$72-?TkpmmOhK(!w?Gbeos4G$K zofF$=ikS=2(-2mwRQ>jo*;F!r`ITUC-!jSlZPx$O3FxKT|CNf>YV7>KxH7;0dj<9X zsV09R1HqVuR4%ZOLVlD>w1LX;Pq2{Sw>lDLod`2jtK#P#y%aZ~%6JqSJU@(_tx{$@ z&if$2!#*20HViXRb1SV2doi_Ps3Vt2G_a#M3kk=SN@>cHZb1bfHH8Czg-)wwS^(K- zA_rx{0O_+>#bo5SAlBN%121V2-0>QDPU?IzU?u>`WTU3`l=1O6Leev5#N~8!KysB* zT{HU;-Ow(B$p`=}h#Mpj_BBv0%fWxy+`&OVrzPW`JI>aVMx5&MnMW*$L>^dO&Tv{1 z)^l;ds9s%z*bo8$%7G1kg`!iBYvL&0#Kj+%PImn|x>EF5Id=)b;@+;uId>IsqCRFa zdmWRtZFH21UIrz)nQ#Eh4nC(!bLb--O=d9iUkz&X;mefHW-_~`NkORM1jMyB2cPqK z6Gm??pUG^sseU`R2P>s!2N5)mH=@J zecQvhLl9T+5=I#28as7a-0X`sOM~tZHcK)ZS|qlYvUK?Qo~NQL#S{LNOISF#OV6CB z<7r0BB2%Gl+vD7S_$QWY7*1ljC;&7Ok~AeI5$992F?=c+A}O?*nY_c9$zc*PjiXiX zjevjR^6`rJn0}GH6r>1s1~M4ObU~^@#YM9_Y+c={5)r2_0##DUGbyS>=n)0Sy56>( zrV0PLFRXzsT|L9^7p6IYe+Guv>|`jSal>}aj33+ha`29Sv9Qd(Jv{6%!#N!GKn!y! zR!ayJi4tjKuLOIo_*QAOI2|mU;mrY5rF7A=F|cs|YXH_9M!eRbu`0eLyRZEdyAr#(KLgYY#r0UJ=4N*$7*)MZ7+_9?~vQaI%_N>%;d%q zT*Pu+i*3Yz2`=hQr3A?MpJz&7v+C5WiE3v3Ixit9H{i}eN`C3diODaL^R<{!{U7#u zrp~|LIhX(Ma;ZG`|NJ`iza0Pg_tNmcQmm{*^}i+b|326Mz6SlT+d*9}>@-mii_t;X zikX39!(O+CO5Y+K)o7U~JKG=atUtnVK;j5SWYV~QXlUqP>1f!}Z5>e~DLq7Nlw-$IgC221u0Ow;pe7K8@#~ zWWSEh-TjAq3Dr=#%(eM=Bc25k<@b%v5BEQc=b&U5kkw{l*r)F*U(R6ow^13<56-T9 zz@cJ)ncck}DUoSgnT*`}zZ-z$OhA&9Fr`eSHGqkq;RtWDWKWe3LVXVyI4fjNMF-Aw zoc21bH_-pgHMWI&0i$buWn$bUekRZ{<;I~8q6w^}J-U#U&x9KxctbO#NIC~Gd?-SH z-wlsR=`j!g75zq?dT84NhF8OwX~0s5hC@<+**EAK&=ITOGG9XduvB(Uwsn7Z4Yp&@ zb={%NB3L5Nm0eiK(i;y;O=pjUx38480q4}O>(}9n$j&1r%N$~~3T7F*#y&H7h+MyK zytBt<&cQBwChQXNR+a^w<|TH46M4*Tw^20@3;M8!swg2*@JxJXxkmT8smP!U*vA%s z1SCA0K}&|?7$;DZB9D2tDAL3262q(QhJVT3vk9Z{570tu2(agZ1aDO2TGLn-ThSq5ApWdk-?+IMYN9zac-vwHs6ueaCsghw#|2 zj?C@}b7}OlUW6K89hR{H2u~#F_3dMS3EVACB<&#{DZdZWmI>o`0sd~&u?8NFomWSM zBMJeyG2SJj;me_o|MKdHL$))aQ7vTU>!P3MCePzzVQm5D_@W%gBSHng+sWf<`dNcf zAx4|t(CCUner3EXILw6wW~~)(GwV+F^qMUA?1SfLkaY7VTU_)*g!=tO82dSYv(|GT z2eNw1;5Q|5NHiHPPEPq1qq&JMj7cD{MRxnPpLzp+3eqhF>85Km>6N08N<490Z9uq0 zuV(9g;$uV9z~Xm7AEFR&06->4E+9U4^K2Vqb01qS;JIrba=i0xLO!6~EbkD_2(e>DHK%>RUjJG{;xahN?9p|MAtoy@W10z(miHW!>KSdRJ7jOZey^g_Kq6W+_ zhmw5(Lwq0Dng~dkmrPW$%0qJus~iCwKIN~m9TR44U$l%^%af%KM#Tz$#w7}7G6L}a z_#&N+j7b3j6-opH0yAll@sa6=o*ep=gZvCHJ0s)uQ5afb89S8{Lui$yRV>^KzMOHA z3>Og%uf?Ip*ctb{5+xG>W)UhrgW|~CZN^(`|D#xakiVKs@c&UMSEBp>YI$kC|GSh& zSoXkX09LWMAr0-$60Y}u?(aMlkxt_ul^~~U8k*3_?T{8S{1YN$yiMtXGPG6Vt*GVK zMwSiAsJ4DQ+_>~Dp!un-i|P5N_-JAw>-Is8!5H)uLAY=O-ed&@%@>7Xc-V?B z_4rw`@P#_i=R%o(-|_6JcrDbZ>KgnjOVwy$6lLT}Dzaaz@zB$rX*g)MEI>^SJB)9z zYu`PCo=TgxFfx3dN=Z`^(L7@!r3#3e9jonS0%|}|Q+ZWy;v4EkxA`d6Xz7Jfr;=Kh zR0OF-QAx5Ebzw&Jh{=SQblWMeKLQ1$rj}FsKvD_!GBR#|a7Ym#>2X5*fY1*xhFvnW z{+@f#*Z4L@V|VY4u1gROAd;)~XN<~VI#1UO}Rha1aP@cx928ya%M5wM*)3YI4 zZ|YRUzC6@_9iRsCRKwLrB;c2>>1SfGDiNy}OboNG5&gJ~V_wWjR>}l>T3#(K6=C{l znRtwCh=G^!gV^IKUxYp^oL3!CaPA&Jf%ho9*cP(b11(ZJ0)rnM2^hdK_pX>J{(9E# z4*M^d#y37S#=RrLqJ!8ayQH9Oa!Fse;n}o(3fDn@nM4(*a#M9pliPA$9idN^^QSCtdd&sW^_yk%ChAoflf+0K$wmN_fGw1@fBzItPx#U3M=4Z4| z;zgOiTN2xUDkS^}F?m^mK4P~C1USFD%J5+u8E!jlEg@)$!!Udlnlcy_o{8uYgHgtz z&7f0%P7|>xG|4#m zrIl__Ln3xl=f~OHFX+YZ|9gYNtlOCW{C~N!GC%)+ZTQb0f&_Kgw;l6R{J)|L*y#Cx zWvM*Z|6Im%W6n|M{9l>>!gO|~H$cbve=b%l(fPkzs?O*C)w%zf!Sp(0T1)lU8oqCT zsgd!J)vOD8LXnz;$xkdsx2LaqSnG80#yy}FzZg{DR z-+9s2cb`1m-q;GVLeM44xR9d~Y zte49A;qHTV?c4x}){;1O9Gc%3#WTq37`MQ`ihk`9(2t`O!)w{5i~hUGe&Lk?AuswB zI^yNBDIfs`^WE6|nLqPq{>-2GGk@mK{Fy)VXa3Be`7?j!&-|G`^Jo5C-RJ)a18(77 I9{@lL0JrvO!2kdN diff --git a/testing/make-archives b/testing/make-archives index ae00d60b6..cb0b0a408 100755 --- a/testing/make-archives +++ b/testing/make-archives @@ -15,11 +15,11 @@ from typing import Sequence REPOS = ( - ('rbenv', 'git://github.com/rbenv/rbenv', '0843745'), - ('ruby-build', 'git://github.com/rbenv/ruby-build', '500863c'), + ('rbenv', 'https://github.com/rbenv/rbenv', '585ed84'), + ('ruby-build', 'https://github.com/rbenv/ruby-build', 'e9fa4bf'), ( 'ruby-download', - 'git://github.com/garnieretienne/rvm-download', + 'https://github.com/garnieretienne/rvm-download', '09bd7c6', ), ) From 229a4e03e3ed531961ca850df6462f19c2a9376d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 21 May 2021 14:04:43 -0700 Subject: [PATCH 520/967] v2.13.0 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 23 +++++++++++++++++++++++ setup.cfg | 2 +- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8276e5120..78cbfacd6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.12.1 + rev: v2.13.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index 50492dfb4..e47a1c5de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +2.13.0 - 2021-05-21 +=================== + +### Features +- Setting `SKIP=...` skips installation as well. + - #1875 PR by @asottile. + - pre-commit-ci/issues#53 issue by @TylerYep. +- Attempt to mount from host with docker-in-docker. + - #1888 PR by @okainov. + - #1387 issue by @okainov. +- Enable `repo: local` for `r` hooks. + - #1878 PR by @lorenzwalthert. +- Upgrade `ruby-build` and `rbenv`. + - #1913 PR by @jalessio. + +### Fixes +- Better detect `r` packages. + - #1898 PR by @lorenzwalthert. +- Avoid warnings with mismatched `renv` versions. + - #1841 PR by @lorenzwalthert. +- Reproducibly produce ruby tar resources. + - #1915 PR by @asottile. + 2.12.1 - 2021-04-16 =================== diff --git a/setup.cfg b/setup.cfg index 400299870..ae5cc7c2e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.12.1 +version = 2.13.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 1d2cde763c8ac884ebdb1008038921ac797850c1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 24 May 2021 17:19:56 +0000 Subject: [PATCH 521/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.16.0 → v2.18.2](https://github.com/asottile/pyupgrade/compare/v2.16.0...v2.18.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 78cbfacd6..9903160ce 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.16.0 + rev: v2.18.2 hooks: - id: pyupgrade args: [--py36-plus] From d3c5cd6ee217235476094eac72e34d18cfd19bd0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 24 May 2021 17:20:16 +0000 Subject: [PATCH 522/967] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/clientlib_test.py | 4 ++-- tests/repository_test.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index ff3cce38d..09bdb3ee9 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -427,7 +427,7 @@ def test_minimum_pre_commit_version_passing(): @pytest.mark.parametrize('schema', (CONFIG_SCHEMA, CONFIG_REPO_DICT)) def test_warn_additional(schema): allowed_keys = {item.key for item in schema.items if hasattr(item, 'key')} - warn_additional, = [ + warn_additional, = ( x for x in schema.items if isinstance(x, cfgv.WarnAdditionalKeys) - ] + ) assert allowed_keys == set(warn_additional.keys) diff --git a/tests/repository_test.py b/tests/repository_test.py index b6f7fb254..af829c2e7 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -51,7 +51,7 @@ def _get_hook_no_install(repo_config, store, hook_id): config = cfgv.validate(config, CONFIG_SCHEMA) config = cfgv.apply_defaults(config, CONFIG_SCHEMA) hooks = all_hooks(config, store) - hook, = [hook for hook in hooks if hook.id == hook_id] + hook, = (hook for hook in hooks if hook.id == hook_id) return hook From b517f9cc7f4fa4a9288182ae7c2ecc28b7466b8a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 31 May 2021 17:26:07 +0000 Subject: [PATCH 523/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.18.2 → v2.19.0](https://github.com/asottile/pyupgrade/compare/v2.18.2...v2.19.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9903160ce..bf1e580b1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.18.2 + rev: v2.19.0 hooks: - id: pyupgrade args: [--py36-plus] From c4e4f2d9fa3807ff019819ea27baebf285857bee Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 8 Jun 2021 01:47:05 +0000 Subject: [PATCH 524/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.19.0 → v2.19.1](https://github.com/asottile/pyupgrade/compare/v2.19.0...v2.19.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bf1e580b1..a2ebb7017 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.19.0 + rev: v2.19.1 hooks: - id: pyupgrade args: [--py36-plus] From 65dc06c9890aa0c200bac8ed6a83c3e7fcb72118 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Jun 2021 17:34:33 +0000 Subject: [PATCH 525/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.19.1 → v2.19.4](https://github.com/asottile/pyupgrade/compare/v2.19.1...v2.19.4) - [github.com/pre-commit/mirrors-mypy: v0.812 → v0.902](https://github.com/pre-commit/mirrors-mypy/compare/v0.812...v0.902) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a2ebb7017..3bd380f92 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.19.1 + rev: v2.19.4 hooks: - id: pyupgrade args: [--py36-plus] @@ -44,7 +44,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.812 + rev: v0.902 hooks: - id: mypy exclude: ^testing/resources/ From 19da6479a8c5e942f68699ecdabb9c75b4497569 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 14 Jun 2021 11:58:41 -0700 Subject: [PATCH 526/967] Add mypy dependency on types-all --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3bd380f92..6c5b2086e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,6 +47,7 @@ repos: rev: v0.902 hooks: - id: mypy + additional_dependencies: [types-all] exclude: ^testing/resources/ - repo: meta hooks: From 0ed646ed0912f709e555a1e90b5457cb1d251dd7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 15 Jun 2021 08:32:44 -0700 Subject: [PATCH 527/967] read legacy hooks in an encoding-agnostic way --- pre_commit/commands/install_uninstall.py | 14 +++++++------- tests/commands/install_uninstall_test.py | 17 +++++++++++++++-- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 684b59805..73c8d6056 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -21,13 +21,13 @@ # This is used to identify the hook file we install PRIOR_HASHES = ( - '4d9958c90bc262f47553e2c073f14cfe', - 'd8ee923c46731b42cd95cc869add4062', - '49fd668cb42069aa1b6048464be5d395', - '79f09a650522a87b0da915d0d983b2de', - 'e358c9dae00eac5d06b38dfdb1e33a8c', + b'4d9958c90bc262f47553e2c073f14cfe', + b'd8ee923c46731b42cd95cc869add4062', + b'49fd668cb42069aa1b6048464be5d395', + b'79f09a650522a87b0da915d0d983b2de', + b'e358c9dae00eac5d06b38dfdb1e33a8c', ) -CURRENT_HASH = '138fd403232d2ddd5efb44317e38bf03' +CURRENT_HASH = b'138fd403232d2ddd5efb44317e38bf03' TEMPLATE_START = '# start templated\n' TEMPLATE_END = '# end templated\n' # Homebrew/homebrew-core#35825: be more timid about appropriate `PATH` @@ -48,7 +48,7 @@ def _hook_paths( def is_our_script(filename: str) -> bool: if not os.path.exists(filename): # pragma: win32 no cover (symlink) return False - with open(filename) as f: + with open(filename, 'rb') as f: contents = f.read() return any(h in contents for h in (CURRENT_HASH,) + PRIOR_HASHES) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index bd28654ff..314b8b969 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -39,7 +39,7 @@ def test_is_script(): def test_is_previous_pre_commit(tmpdir): f = tmpdir.join('foo') - f.write(f'{PRIOR_HASHES[0]}\n') + f.write(f'{PRIOR_HASHES[0].decode()}\n') assert is_our_script(f.strpath) @@ -390,6 +390,19 @@ def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory, store): NORMAL_PRE_COMMIT_RUN.assert_matches(output[len('legacy hook\n'):]) +def test_install_with_existing_non_utf8_script(tmpdir, store): + cmd_output('git', 'init', str(tmpdir)) + tmpdir.join('.git/hooks').ensure_dir() + tmpdir.join('.git/hooks/pre-commit').write_binary( + b'#!/usr/bin/env bash\n' + b'# garbage: \xa0\xef\x12\xf2\n' + b'echo legacy hook\n', + ) + + with tmpdir.as_cwd(): + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 + + FAIL_OLD_HOOK = re_assert.Matches( r'fail!\n' r'\[INFO\] Initializing environment for .+\.\n' @@ -460,7 +473,7 @@ def test_replace_old_commit_script(tempdir_factory, store): # Install a script that looks like our old script pre_commit_contents = resource_text('hook-tmpl') new_contents = pre_commit_contents.replace( - CURRENT_HASH, PRIOR_HASHES[-1], + CURRENT_HASH.decode(), PRIOR_HASHES[-1].decode(), ) os.makedirs(os.path.join(path, '.git/hooks'), exist_ok=True) From 584fd585eca1159e441c640e3366df8257cce8af Mon Sep 17 00:00:00 2001 From: Florent Clarret Date: Sat, 19 Jun 2021 18:09:32 +0200 Subject: [PATCH 528/967] Expose local branch ref as an environment variable --- pre_commit/commands/hook_impl.py | 7 ++++++- pre_commit/commands/run.py | 6 +++++- pre_commit/main.py | 3 +++ testing/util.py | 2 ++ tests/commands/run_test.py | 1 + 5 files changed, 17 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index a766ee9d6..c544167c1 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -70,6 +70,7 @@ def _ns( *, all_files: bool = False, remote_branch: Optional[str] = None, + local_branch: Optional[str] = None, from_ref: Optional[str] = None, to_ref: Optional[str] = None, remote_name: Optional[str] = None, @@ -82,6 +83,7 @@ def _ns( color=color, hook_stage=hook_type.replace('pre-', ''), remote_branch=remote_branch, + local_branch=local_branch, from_ref=from_ref, to_ref=to_ref, remote_name=remote_name, @@ -110,7 +112,7 @@ def _pre_push_ns( remote_url = args[1] for line in stdin.decode().splitlines(): - _, local_sha, remote_branch, remote_sha = line.split() + local_branch, local_sha, remote_branch, remote_sha = line.split() if local_sha == Z40: continue elif remote_sha != Z40 and _rev_exists(remote_sha): @@ -118,6 +120,7 @@ def _pre_push_ns( 'pre-push', color, from_ref=remote_sha, to_ref=local_sha, remote_branch=remote_branch, + local_branch=local_branch, remote_name=remote_name, remote_url=remote_url, ) else: @@ -139,6 +142,7 @@ def _pre_push_ns( all_files=True, remote_name=remote_name, remote_url=remote_url, remote_branch=remote_branch, + local_branch=local_branch, ) else: rev_cmd = ('git', 'rev-parse', f'{first_ancestor}^') @@ -148,6 +152,7 @@ def _pre_push_ns( from_ref=source, to_ref=local_sha, remote_name=remote_name, remote_url=remote_url, remote_branch=remote_branch, + local_branch=local_branch, ) # nothing to push diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 0fef50d1c..d906d5b83 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -371,7 +371,11 @@ def run( environ['PRE_COMMIT_FROM_REF'] = args.from_ref environ['PRE_COMMIT_TO_REF'] = args.to_ref - if args.remote_name and args.remote_url and args.remote_branch: + if ( + args.remote_name and args.remote_url and + args.remote_branch and args.local_branch + ): + environ['PRE_COMMIT_LOCAL_BRANCH'] = args.local_branch environ['PRE_COMMIT_REMOTE_BRANCH'] = args.remote_branch environ['PRE_COMMIT_REMOTE_NAME'] = args.remote_name environ['PRE_COMMIT_REMOTE_URL'] = args.remote_url diff --git a/pre_commit/main.py b/pre_commit/main.py index c66cfb9a4..ad3d87370 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -99,6 +99,9 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: parser.add_argument( '--remote-branch', help='Remote branch ref used by `git push`.', ) + parser.add_argument( + '--local-branch', help='Local branch ref used by `git push`.', + ) parser.add_argument( '--from-ref', '--source', '-s', help=( diff --git a/testing/util.py b/testing/util.py index 13644531d..12f22b59d 100644 --- a/testing/util.py +++ b/testing/util.py @@ -62,6 +62,7 @@ def run_opts( verbose=False, hook=None, remote_branch='', + local_branch='', from_ref='', to_ref='', remote_name='', @@ -81,6 +82,7 @@ def run_opts( verbose=verbose, hook=hook, remote_branch=remote_branch, + local_branch=local_branch, from_ref=from_ref, to_ref=to_ref, remote_name=remote_name, diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index e184340c2..da7569ed0 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -487,6 +487,7 @@ def test_all_push_options_ok(cap_out, store, repo_with_passing_hook): args = run_opts( from_ref='master', to_ref='master', remote_branch='master', + local_branch='master', remote_name='origin', remote_url='https://example.com/repo', ) ret, printed = _do_run(cap_out, store, repo_with_passing_hook, args) From 1dca1f3c19a615a6a5150a6d50a2cee273773609 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 21 Jun 2021 19:15:37 -0700 Subject: [PATCH 529/967] stricter mypy settings Committed via https://github.com/asottile/all-repos --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.cfg b/setup.cfg index ae5cc7c2e..c011514e7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -63,6 +63,8 @@ disallow_any_generics = true disallow_incomplete_defs = true disallow_untyped_defs = true no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true [mypy-testing.*] disallow_untyped_defs = false From af429b951da0ea8ff997a6fa9ca0aa65eeb4ce80 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 28 Jun 2021 17:37:10 +0000 Subject: [PATCH 530/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.902 → v0.910](https://github.com/pre-commit/mirrors-mypy/compare/v0.902...v0.910) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6c5b2086e..525cebc21 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.902 + rev: v0.910 hooks: - id: mypy additional_dependencies: [types-all] From 3e1020945ee195913839e0865ec71b3539212e33 Mon Sep 17 00:00:00 2001 From: Adar Nimrod Date: Sat, 22 May 2021 13:13:33 +0300 Subject: [PATCH 531/967] A more reliable way to get the container id. The hostname is not always the container id. Get the container id from /proc/1/cgroup. Fixes #1918. --- pre_commit/languages/docker.py | 17 +++++-- tests/languages/docker_test.py | 84 +++++++++++++++++++++------------- 2 files changed, 65 insertions(+), 36 deletions(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 5b21ec94c..5bb3395af 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -1,7 +1,6 @@ import hashlib import json import os -import socket from typing import Sequence from typing import Tuple @@ -26,12 +25,24 @@ def _is_in_docker() -> bool: return False +def _get_container_id() -> str: + # It's assumed that we already check /proc/1/cgroup in _is_in_docker. The + # cpuset cgroup controller existed since cgroups were introduced so this + # way of getting the container ID is pretty reliable. + with open('/proc/1/cgroup', 'rb') as f: + for line in f.readlines(): + if line.split(b':')[1] == b'cpuset': + return os.path.basename(line.split(b':')[2]).strip().decode() + raise RuntimeError('Failed to find the container ID in /proc/1/cgroup.') + + def _get_docker_path(path: str) -> str: if not _is_in_docker(): return path - hostname = socket.gethostname() - _, out, _ = cmd_output_b('docker', 'inspect', hostname) + container_id = _get_container_id() + + _, out, _ = cmd_output_b('docker', 'inspect', container_id) container, = json.loads(out) for mount in container['Mounts']: diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 01b5e2773..06a08bc9f 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -9,6 +9,41 @@ from pre_commit.languages import docker +DOCKER_CGROUP_EXAMPLE = b'''\ +12:hugetlb:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +11:blkio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +10:freezer:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +9:cpu,cpuacct:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +8:pids:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +7:rdma:/ +6:net_cls,net_prio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +5:cpuset:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +4:devices:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +3:memory:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +2:perf_event:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +1:name=systemd:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +0::/system.slice/containerd.service +''' # noqa: E501 + +# The ID should match the above cgroup example. +CONTAINER_ID = 'c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7' # noqa: E501 + +NON_DOCKER_CGROUP_EXAMPLE = b'''\ +12:perf_event:/ +11:hugetlb:/ +10:devices:/ +9:blkio:/ +8:rdma:/ +7:cpuset:/ +6:cpu,cpuacct:/ +5:freezer:/ +4:memory:/ +3:pids:/ +2:net_cls,net_prio:/ +1:name=systemd:/init.scope +0::/init.scope +''' + def test_docker_fallback_user(): def invalid_attribute(): @@ -37,45 +72,25 @@ def _mock_open(data): def test_in_docker_docker_in_file(): - docker_cgroup_example = b'''\ -12:hugetlb:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -11:blkio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -10:freezer:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -9:cpu,cpuacct:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -8:pids:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -7:rdma:/ -6:net_cls,net_prio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -5:cpuset:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -4:devices:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -3:memory:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -2:perf_event:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -1:name=systemd:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -0::/system.slice/containerd.service -''' # noqa: E501 - with _mock_open(docker_cgroup_example): + with _mock_open(DOCKER_CGROUP_EXAMPLE): assert docker._is_in_docker() is True def test_in_docker_docker_not_in_file(): - non_docker_cgroup_example = b'''\ -12:perf_event:/ -11:hugetlb:/ -10:devices:/ -9:blkio:/ -8:rdma:/ -7:cpuset:/ -6:cpu,cpuacct:/ -5:freezer:/ -4:memory:/ -3:pids:/ -2:net_cls,net_prio:/ -1:name=systemd:/init.scope -0::/init.scope -''' - with _mock_open(non_docker_cgroup_example): + with _mock_open(NON_DOCKER_CGROUP_EXAMPLE): assert docker._is_in_docker() is False +def test_get_container_id(): + with _mock_open(DOCKER_CGROUP_EXAMPLE): + assert docker._get_container_id() == CONTAINER_ID + + +def test_get_container_id_failure(): + with _mock_open(b''), pytest.raises(RuntimeError): + docker._get_container_id() + + def test_get_docker_path_not_in_docker_returns_same(): with mock.patch.object(docker, '_is_in_docker', return_value=False): assert docker._get_docker_path('abc') == 'abc' @@ -84,7 +99,10 @@ def test_get_docker_path_not_in_docker_returns_same(): @pytest.fixture def in_docker(): with mock.patch.object(docker, '_is_in_docker', return_value=True): - yield + with mock.patch.object( + docker, '_get_container_id', return_value=CONTAINER_ID, + ): + yield def _linux_commonpath(): From 8067f013d2ec2a99a9721ab125d7ab052ec2880e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 3 Jul 2021 11:14:05 -0700 Subject: [PATCH 532/967] fix casing in .pre-commit-hooks.yaml --- .pre-commit-hooks.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index ef269d133..3d1ffbcbb 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -1,5 +1,5 @@ - id: validate_manifest - name: Validate Pre-Commit Manifest + name: validate pre-commit manifest description: This validator validates a pre-commit hooks manifest file entry: pre-commit-validate-manifest language: python From d4c14fb6fddf8084ad0dee29b12b21b660483ab5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Jul 2021 22:01:40 +0000 Subject: [PATCH 533/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.19.4 → v2.20.0](https://github.com/asottile/pyupgrade/compare/v2.19.4...v2.20.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 525cebc21..171554f4d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.19.4 + rev: v2.20.0 hooks: - id: pyupgrade args: [--py36-plus] From 81c0413c38cf8e292050edda39b4255fe7af05ad Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 12 Jul 2021 23:20:49 +0000 Subject: [PATCH 534/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.20.0 → v2.21.0](https://github.com/asottile/pyupgrade/compare/v2.20.0...v2.21.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 171554f4d..3098fdd00 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.20.0 + rev: v2.21.0 hooks: - id: pyupgrade args: [--py36-plus] From 0065a71e3d4347f0c11a9d3f53257bc92f284934 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Jul 2021 21:58:13 +0000 Subject: [PATCH 535/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.21.0 → v2.21.2](https://github.com/asottile/pyupgrade/compare/v2.21.0...v2.21.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3098fdd00..1e6461aeb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.21.0 + rev: v2.21.2 hooks: - id: pyupgrade args: [--py36-plus] From 6cd4e2af48fb1754a762864a5d4b2beab8237f73 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 26 Jul 2021 17:51:54 +0000 Subject: [PATCH 536/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.21.2 → v2.23.0](https://github.com/asottile/pyupgrade/compare/v2.21.2...v2.23.0) - [github.com/asottile/reorder_python_imports: v2.5.0 → v2.6.0](https://github.com/asottile/reorder_python_imports/compare/v2.5.0...v2.6.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1e6461aeb..6fb6091b9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,12 +25,12 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.21.2 + rev: v2.23.0 hooks: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/asottile/reorder_python_imports - rev: v2.5.0 + rev: v2.6.0 hooks: - id: reorder-python-imports args: [--py3-plus] From 5bd2077872f6b41276f83208647b136843e65a4b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Aug 2021 18:01:06 +0000 Subject: [PATCH 537/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.23.0 → v2.23.1](https://github.com/asottile/pyupgrade/compare/v2.23.0...v2.23.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6fb6091b9..543313001 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.23.0 + rev: v2.23.1 hooks: - id: pyupgrade args: [--py36-plus] From 5d1cac64c1953663959fb71f46dff0179bf63f9a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 3 Aug 2021 13:08:07 -0700 Subject: [PATCH 538/967] ignore self-container when in docker-in-docker --- pre_commit/languages/docker.py | 7 ++++++- tests/languages/docker_test.py | 8 ++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 5bb3395af..644d8d293 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -8,6 +8,7 @@ from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix +from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b @@ -42,7 +43,11 @@ def _get_docker_path(path: str) -> str: container_id = _get_container_id() - _, out, _ = cmd_output_b('docker', 'inspect', container_id) + try: + _, out, _ = cmd_output_b('docker', 'inspect', container_id) + except CalledProcessError: + # self-container was not visible from here (perhaps docker-in-docker) + return path container, = json.loads(out) for mount in container['Mounts']: diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 06a08bc9f..ec6bb83ca 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -8,6 +8,7 @@ import pytest from pre_commit.languages import docker +from pre_commit.util import CalledProcessError DOCKER_CGROUP_EXAMPLE = b'''\ 12:hugetlb:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 @@ -171,3 +172,10 @@ def test_get_docker_path_in_docker_windows(in_docker): path = r'c:\folder\test\something' expected = r'c:\users\user\test\something' assert docker._get_docker_path(path) == expected + + +def test_get_docker_path_in_docker_docker_in_docker(in_docker): + # won't be able to discover "self" container in true docker-in-docker + err = CalledProcessError(1, (), 0, b'', b'') + with mock.patch.object(docker, 'cmd_output_b', side_effect=err): + assert docker._get_docker_path('/project') == '/project' From ab15d7d22d01c21d5fb0c6fa03ff17a92d87d315 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 6 Aug 2021 11:32:11 -0700 Subject: [PATCH 539/967] v2.14.0 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 19 +++++++++++++++++++ setup.cfg | 2 +- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 543313001..8c06de56d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.13.0 + rev: v2.14.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index e47a1c5de..eaeaa27bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +2.14.0 - 2021-08-06 +=================== + +### Features +- During `pre-push` hooks, expose local branch as `PRE_COMMIT_LOCAL_BRANCH`. + - #1947 PR by @FlorentClarret. + - #1410 issue by @MaicoTimmerman. +- Improve container id detection for docker-beside-docker with custom hostname. + - #1919 PR by @adarnimrod. + - #1918 issue by @adarnimrod. + +### Fixes +- Read legacy hooks in an encoding-agnostic way. + - #1943 PR by @asottile. + - #1942 issue by @sbienkow-ninja. +- Fix execution of docker hooks for docker-in-docker. + - #1997 PR by @asottile. + - #1978 issue by @robin-moss. + 2.13.0 - 2021-05-21 =================== diff --git a/setup.cfg b/setup.cfg index c011514e7..50f160531 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.13.0 +version = 2.14.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From d6f5504311352a8dd6c71e884a862869428e5aca Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 Aug 2021 18:05:46 +0000 Subject: [PATCH 540/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.23.1 → v2.23.3](https://github.com/asottile/pyupgrade/compare/v2.23.1...v2.23.3) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8c06de56d..2b3ed777e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.23.1 + rev: v2.23.3 hooks: - id: pyupgrade args: [--py36-plus] From 0fe959df6040ef76d7ccfd93c8f4c14c5115f4a1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 9 Aug 2021 20:13:15 -0400 Subject: [PATCH 541/967] fall back to full diff on disparate histories --- pre_commit/git.py | 15 +++++++++------ tests/git_test.py | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 4bf282357..6264529d5 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -155,12 +155,15 @@ def get_all_files() -> List[str]: def get_changed_files(old: str, new: str) -> List[str]: - return zsplit( - cmd_output( - 'git', 'diff', '--name-only', '--no-ext-diff', '-z', - f'{old}...{new}', - )[1], - ) + diff_cmd = ('git', 'diff', '--name-only', '--no-ext-diff', '-z') + try: + _, out, _ = cmd_output(*diff_cmd, f'{old}...{new}') + except CalledProcessError: # pragma: no cover (new git) + # on newer git where old and new do not have a merge base git fails + # so we try a full diff (this is what old git did for us!) + _, out, _ = cmd_output(*diff_cmd, f'{old}..{new}') + + return zsplit(out) def head_rev(remote: str) -> str: diff --git a/tests/git_test.py b/tests/git_test.py index 51d5f8c43..aa218804c 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -139,6 +139,24 @@ def test_get_changed_files(in_git_dir): assert files == [] +def test_get_changed_files_disparate_histories(in_git_dir): + """in modern versions of git, `...` does not fall back to full diff""" + git_commit() + in_git_dir.join('a.txt').ensure() + cmd_output('git', 'add', '.') + git_commit() + cmd_output('git', 'branch', '-m', 'branch1') + + cmd_output('git', 'checkout', '--orphan', 'branch2') + cmd_output('git', 'rm', '-rf', '.') + in_git_dir.join('a.txt').ensure() + in_git_dir.join('b.txt').ensure() + cmd_output('git', 'add', '.') + git_commit() + + assert git.get_changed_files('branch1', 'branch2') == ['b.txt'] + + @pytest.mark.parametrize( ('s', 'expected'), ( From b829bc2dbad04043700c2f33c315b0c698400a0d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Aug 2021 18:12:36 +0000 Subject: [PATCH 542/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.23.3 → v2.24.0](https://github.com/asottile/pyupgrade/compare/v2.23.3...v2.24.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2b3ed777e..28529e910 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.23.3 + rev: v2.24.0 hooks: - id: pyupgrade args: [--py36-plus] From f963bf6f9a403837e75f8bcaa869bb0f64170b4f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Aug 2021 13:39:55 -0400 Subject: [PATCH 543/967] make `repo: meta` only apply to top level configuration --- pre_commit/clientlib.py | 4 ++-- tests/clientlib_test.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 962c7fa8f..bc7274a7d 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -216,14 +216,14 @@ def warn_unknown_keys_repo( ( 'check-hooks-apply', ( ('name', 'Check hooks apply to the repository'), - ('files', C.CONFIG_FILE), + ('files', f'^{re.escape(C.CONFIG_FILE)}$'), ('entry', _entry('check_hooks_apply')), ), ), ( 'check-useless-excludes', ( ('name', 'Check for useless excludes'), - ('files', C.CONFIG_FILE), + ('files', f'^{re.escape(C.CONFIG_FILE)}$'), ('entry', _entry('check_useless_excludes')), ), ), diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 09bdb3ee9..da794e6e7 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -1,4 +1,5 @@ import logging +import re import cfgv import pytest @@ -10,6 +11,7 @@ from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import DEFAULT_LANGUAGE_VERSION from pre_commit.clientlib import MANIFEST_SCHEMA +from pre_commit.clientlib import META_HOOK_DICT from pre_commit.clientlib import MigrateShaToRev from pre_commit.clientlib import validate_config_main from pre_commit.clientlib import validate_manifest_main @@ -392,6 +394,15 @@ def test_meta_hook_invalid(config_repo): cfgv.validate(config_repo, CONFIG_REPO_DICT) +def test_meta_check_hooks_apply_only_at_top_level(): + cfg = {'id': 'check-hooks-apply'} + cfg = cfgv.apply_defaults(cfg, META_HOOK_DICT) + + files_re = re.compile(cfg['files']) + assert files_re.search('.pre-commit-config.yaml') + assert not files_re.search('foo/.pre-commit-config.yaml') + + @pytest.mark.parametrize( 'mapping', ( From 0f08ba77c856f75a248bec6b2b087a043f285748 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Aug 2021 14:15:40 -0400 Subject: [PATCH 544/967] v2.14.1 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 12 ++++++++++++ setup.cfg | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 28529e910..6dd99d78d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.14.0 + rev: v2.14.1 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index eaeaa27bf..f77ec9518 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +2.14.1 - 2021-08-28 +=================== + +### Fixes +- fix force-push of disparate histories using git>=2.28. + - #2005 PR by @asottile. + - #2002 issue by @bogusfocused. +- fix `check-useless-excludes` and `check-hooks-apply` matching non-root + `.pre-commit-config.yaml`. + - #2026 PR by @asottile. + - pre-commit-ci/issues#84 issue by @billsioros. + 2.14.0 - 2021-08-06 =================== diff --git a/setup.cfg b/setup.cfg index 50f160531..47fa310f2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.14.0 +version = 2.14.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From f8e21cb78bbc885eb0afe12ec69b5358e44ddf89 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Aug 2021 18:44:34 -0400 Subject: [PATCH 545/967] add support for dart as a hook language --- azure-pipelines.yml | 26 ++--- pre_commit/languages/all.py | 2 + pre_commit/languages/dart.py | 109 ++++++++++++++++++ pre_commit/languages/helpers.py | 4 +- pre_commit/languages/python.py | 3 +- .../resources/empty_template_pubspec.yaml | 4 + pre_commit/store.py | 2 +- pre_commit/util.py | 4 + testing/gen-languages-all | 6 +- testing/get-coursier.sh | 4 +- testing/get-dart.sh | 17 +++ testing/get-swift.sh | 4 +- .../dart_repo/.pre-commit-hooks.yaml | 4 + .../dart_repo/bin/hello-world-dart.dart | 6 + testing/resources/dart_repo/pubspec.yaml | 10 ++ .../.pre-commit-hooks.yaml | 2 +- .../.pre-commit-hooks.yaml | 2 +- tests/languages/python_test.py | 5 +- tests/repository_test.py | 42 ++++++- 19 files changed, 227 insertions(+), 29 deletions(-) create mode 100644 pre_commit/languages/dart.py create mode 100644 pre_commit/resources/empty_template_pubspec.yaml create mode 100755 testing/get-dart.sh create mode 100644 testing/resources/dart_repo/.pre-commit-hooks.yaml create mode 100644 testing/resources/dart_repo/bin/hello-world-dart.dart create mode 100644 testing/resources/dart_repo/pubspec.yaml diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 58dee74a8..a468e8aa6 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -26,9 +26,9 @@ jobs: Write-Host "##vso[task.prependpath]C:\Strawberry\perl\site\bin" Write-Host "##vso[task.prependpath]C:\Strawberry\c\bin" displayName: Add strawberry perl to PATH - - task: PowerShell@2 - inputs: - filePath: "testing/get-r.ps1" + - bash: testing/get-dart.sh + displayName: install dart + - powershell: testing/get-r.ps1 displayName: install R - template: job--python-tox.yml@asottile parameters: @@ -38,13 +38,11 @@ jobs: pre_test: - task: UseRubyVersion@0 - template: step--git-install.yml - - bash: | - testing/get-coursier.sh - echo '##vso[task.prependpath]/tmp/coursier' + - bash: testing/get-coursier.sh displayName: install coursier - - bash: | - testing/get-swift.sh - echo '##vso[task.prependpath]/tmp/swift/usr/bin' + - bash: testing/get-dart.sh + displayName: install dart + - bash: testing/get-swift.sh displayName: install swift - bash: testing/get-r.sh displayName: install R @@ -54,13 +52,11 @@ jobs: os: linux pre_test: - task: UseRubyVersion@0 - - bash: | - testing/get-coursier.sh - echo '##vso[task.prependpath]/tmp/coursier' + - bash: testing/get-coursier.sh displayName: install coursier - - bash: | - testing/get-swift.sh - echo '##vso[task.prependpath]/tmp/swift/usr/bin' + - bash: testing/get-dart.sh + displayName: install dart + - bash: testing/get-swift.sh displayName: install swift - bash: testing/get-r.sh displayName: install R diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index fde6000cb..d8a364c5d 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -7,6 +7,7 @@ from pre_commit.hook import Hook from pre_commit.languages import conda from pre_commit.languages import coursier +from pre_commit.languages import dart from pre_commit.languages import docker from pre_commit.languages import docker_image from pre_commit.languages import dotnet @@ -44,6 +45,7 @@ class Language(NamedTuple): # BEGIN GENERATED (testing/gen-languages-all) 'conda': Language(name='conda', ENVIRONMENT_DIR=conda.ENVIRONMENT_DIR, get_default_version=conda.get_default_version, healthy=conda.healthy, install_environment=conda.install_environment, run_hook=conda.run_hook), # noqa: E501 'coursier': Language(name='coursier', ENVIRONMENT_DIR=coursier.ENVIRONMENT_DIR, get_default_version=coursier.get_default_version, healthy=coursier.healthy, install_environment=coursier.install_environment, run_hook=coursier.run_hook), # noqa: E501 + 'dart': Language(name='dart', ENVIRONMENT_DIR=dart.ENVIRONMENT_DIR, get_default_version=dart.get_default_version, healthy=dart.healthy, install_environment=dart.install_environment, run_hook=dart.run_hook), # noqa: E501 'docker': Language(name='docker', ENVIRONMENT_DIR=docker.ENVIRONMENT_DIR, get_default_version=docker.get_default_version, healthy=docker.healthy, install_environment=docker.install_environment, run_hook=docker.run_hook), # noqa: E501 'docker_image': Language(name='docker_image', ENVIRONMENT_DIR=docker_image.ENVIRONMENT_DIR, get_default_version=docker_image.get_default_version, healthy=docker_image.healthy, install_environment=docker_image.install_environment, run_hook=docker_image.run_hook), # noqa: E501 'dotnet': Language(name='dotnet', ENVIRONMENT_DIR=dotnet.ENVIRONMENT_DIR, get_default_version=dotnet.get_default_version, healthy=dotnet.healthy, install_environment=dotnet.install_environment, run_hook=dotnet.run_hook), # noqa: E501 diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py new file mode 100644 index 000000000..16e755461 --- /dev/null +++ b/pre_commit/languages/dart.py @@ -0,0 +1,109 @@ +import contextlib +import os.path +import shutil +import tempfile +from typing import Generator +from typing import Sequence +from typing import Tuple + +import pre_commit.constants as C +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import Var +from pre_commit.hook import Hook +from pre_commit.languages import helpers +from pre_commit.prefix import Prefix +from pre_commit.util import clean_path_on_failure +from pre_commit.util import win_exe +from pre_commit.util import yaml_load + +ENVIRONMENT_DIR = 'dartenv' + +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy + + +def get_env_patch(venv: str) -> PatchesT: + return ( + ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), + ) + + +@contextlib.contextmanager +def in_env(prefix: Prefix) -> Generator[None, None, None]: + directory = helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT) + envdir = prefix.path(directory) + with envcontext(get_env_patch(envdir)): + yield + + +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: + helpers.assert_version_default('dart', version) + + envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) + bin_dir = os.path.join(envdir, 'bin') + + def _install_dir(prefix_p: Prefix, pub_cache: str) -> None: + dart_env = {**os.environ, 'PUB_CACHE': pub_cache} + + with open(prefix_p.path('pubspec.yaml')) as f: + pubspec_contents = yaml_load(f) + + helpers.run_setup_cmd(prefix_p, ('dart', 'pub', 'get'), env=dart_env) + + for executable in pubspec_contents['executables']: + helpers.run_setup_cmd( + prefix_p, + ( + 'dart', 'compile', 'exe', + '--output', os.path.join(bin_dir, win_exe(executable)), + prefix_p.path('bin', f'{executable}.dart'), + ), + env=dart_env, + ) + + with clean_path_on_failure(envdir): + os.makedirs(bin_dir) + + with tempfile.TemporaryDirectory() as tmp: + _install_dir(prefix, tmp) + + for dep_s in additional_dependencies: + with tempfile.TemporaryDirectory() as dep_tmp: + dep, _, version = dep_s.partition(':') + if version: + dep_cmd: Tuple[str, ...] = (dep, '--version', version) + else: + dep_cmd = (dep,) + + helpers.run_setup_cmd( + prefix, + ('dart', 'pub', 'cache', 'add', *dep_cmd), + env={**os.environ, 'PUB_CACHE': dep_tmp}, + ) + + # try and find the 'pubspec.yaml' that just got added + for root, _, filenames in os.walk(dep_tmp): + if 'pubspec.yaml' in filenames: + with tempfile.TemporaryDirectory() as copied: + pkg = os.path.join(copied, 'pkg') + shutil.copytree(root, pkg) + _install_dir(Prefix(pkg), dep_tmp) + break + else: + raise AssertionError( + f'could not find pubspec.yaml for {dep_s}', + ) + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: + with in_env(hook.prefix): + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 29138fd1a..276ce1611 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -48,8 +48,8 @@ def exe_exists(exe: str) -> bool: ) -def run_setup_cmd(prefix: Prefix, cmd: Tuple[str, ...]) -> None: - cmd_output_b(*cmd, cwd=prefix.prefix_dir) +def run_setup_cmd(prefix: Prefix, cmd: Tuple[str, ...], **kwargs: Any) -> None: + cmd_output_b(*cmd, cwd=prefix.prefix_dir, **kwargs) @overload diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 43b728082..faa602977 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -21,6 +21,7 @@ from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b +from pre_commit.util import win_exe ENVIRONMENT_DIR = 'py_env' @@ -172,7 +173,7 @@ def healthy(prefix: Prefix, language_version: str) -> bool: if not os.path.exists(pyvenv_cfg): return False - exe_name = 'python.exe' if sys.platform == 'win32' else 'python' + exe_name = win_exe('python') py_exe = prefix.path(bin_dir(envdir), exe_name) cfg = _read_pyvenv_cfg(pyvenv_cfg) diff --git a/pre_commit/resources/empty_template_pubspec.yaml b/pre_commit/resources/empty_template_pubspec.yaml new file mode 100644 index 000000000..3be6ffe36 --- /dev/null +++ b/pre_commit/resources/empty_template_pubspec.yaml @@ -0,0 +1,4 @@ +name: pre_commit_empty_pubspec +environment: + sdk: '>=2.10.0' +executables: {} diff --git a/pre_commit/store.py b/pre_commit/store.py index 0fd5e6238..fc3bc511a 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -189,7 +189,7 @@ def _git_cmd(*args: str) -> None: LOCAL_RESOURCES = ( 'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore', 'package.json', 'pre_commit_placeholder_package.gemspec', 'setup.py', - 'environment.yml', 'Makefile.PL', + 'environment.yml', 'Makefile.PL', 'pubspec.yaml', 'renv.lock', 'renv/activate.R', 'renv/LICENSE.renv', ) diff --git a/pre_commit/util.py b/pre_commit/util.py index b5f40ada4..6bf8ae7a1 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -268,3 +268,7 @@ def handle_remove_readonly( def parse_version(s: str) -> Tuple[int, ...]: """poor man's version comparison""" return tuple(int(p) for p in s.split('.')) + + +def win_exe(s: str) -> str: + return s if sys.platform != 'win32' else f'{s}.exe' diff --git a/testing/gen-languages-all b/testing/gen-languages-all index eb7cd701e..51e4bce62 100755 --- a/testing/gen-languages-all +++ b/testing/gen-languages-all @@ -2,9 +2,9 @@ import sys LANGUAGES = [ - 'conda', 'coursier', 'docker', 'docker_image', 'dotnet', 'fail', 'golang', - 'node', 'perl', 'pygrep', 'python', 'r', 'ruby', 'rust', 'script', - 'swift', 'system', + 'conda', 'coursier', 'dart', 'docker', 'docker_image', 'dotnet', 'fail', + 'golang', 'node', 'perl', 'pygrep', 'python', 'r', 'ruby', 'rust', + 'script', 'swift', 'system', ] FIELDS = [ 'ENVIRONMENT_DIR', 'get_default_version', 'healthy', 'install_environment', diff --git a/testing/get-coursier.sh b/testing/get-coursier.sh index 760c6c125..4c5e955de 100755 --- a/testing/get-coursier.sh +++ b/testing/get-coursier.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # This is a script used in CI to install coursier -set -euxo pipefail +set -euo pipefail COURSIER_URL="https://github.com/coursier/coursier/releases/download/v2.0.0/cs-x86_64-pc-linux" COURSIER_HASH="e2e838b75bc71b16bcb77ce951ad65660c89bda7957c79a0628ec7146d35122f" @@ -11,3 +11,5 @@ rm -f "$ARTIFACT" curl --location --silent --output "$ARTIFACT" "$COURSIER_URL" echo "$COURSIER_HASH $ARTIFACT" | sha256sum --check chmod ugo+x /tmp/coursier/cs + +echo '##vso[task.prependpath]/tmp/coursier' diff --git a/testing/get-dart.sh b/testing/get-dart.sh new file mode 100755 index 000000000..b655e1a8d --- /dev/null +++ b/testing/get-dart.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail + +VERSION=2.13.4 + +if [ "$OSTYPE" = msys ]; then + URL="https://storage.googleapis.com/dart-archive/channels/stable/release/${VERSION}/sdk/dartsdk-windows-x64-release.zip" + echo "##vso[task.prependpath]$(cygpath -w /tmp/dart-sdk/bin)" +else + URL="https://storage.googleapis.com/dart-archive/channels/stable/release/${VERSION}/sdk/dartsdk-linux-x64-release.zip" + echo '##vso[task.prependpath]/tmp/dart-sdk/bin' +fi + +curl --silent --location --output /tmp/dart.zip "$URL" + +unzip -q -d /tmp /tmp/dart.zip +rm /tmp/dart.zip diff --git a/testing/get-swift.sh b/testing/get-swift.sh index e205d44e2..a05b7b9e6 100755 --- a/testing/get-swift.sh +++ b/testing/get-swift.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # This is a script used in CI to install swift -set -euxo pipefail +set -euo pipefail . /etc/lsb-release if [ "$DISTRIB_CODENAME" = "bionic" ]; then @@ -25,3 +25,5 @@ fi mkdir -p /tmp/swift tar -xf "$TGZ" --strip 1 --directory /tmp/swift + +echo '##vso[task.prependpath]/tmp/swift/usr/bin' diff --git a/testing/resources/dart_repo/.pre-commit-hooks.yaml b/testing/resources/dart_repo/.pre-commit-hooks.yaml new file mode 100644 index 000000000..e0dc5a2a9 --- /dev/null +++ b/testing/resources/dart_repo/.pre-commit-hooks.yaml @@ -0,0 +1,4 @@ +- id: hello-world-dart + name: hello world dart + entry: hello-world-dart + language: dart diff --git a/testing/resources/dart_repo/bin/hello-world-dart.dart b/testing/resources/dart_repo/bin/hello-world-dart.dart new file mode 100644 index 000000000..5d8d6a6af --- /dev/null +++ b/testing/resources/dart_repo/bin/hello-world-dart.dart @@ -0,0 +1,6 @@ +import 'package:ansicolor/ansicolor.dart'; + +void main() { + AnsiPen pen = new AnsiPen()..red(); + print("hello hello " + pen("world")); +} diff --git a/testing/resources/dart_repo/pubspec.yaml b/testing/resources/dart_repo/pubspec.yaml new file mode 100644 index 000000000..bc719d055 --- /dev/null +++ b/testing/resources/dart_repo/pubspec.yaml @@ -0,0 +1,10 @@ +environment: + sdk: '>=2.10.0 <3.0.0' + +name: hello_world_dart + +executables: + hello-world-dart: + +dependencies: + ansicolor: ^2.0.1 diff --git a/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml index d005a74cc..0f514c116 100644 --- a/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml +++ b/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml @@ -1,4 +1,4 @@ -- id: dotnet example hook +- id: dotnet-example-hook name: dotnet example hook entry: testeroni language: dotnet diff --git a/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml index d005a74cc..0f514c116 100644 --- a/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml +++ b/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml @@ -1,4 +1,4 @@ -- id: dotnet example hook +- id: dotnet-example-hook name: dotnet example hook entry: testeroni language: dotnet diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 90d1036a3..8324cac20 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -9,6 +9,7 @@ from pre_commit.languages import python from pre_commit.prefix import Prefix from pre_commit.util import make_executable +from pre_commit.util import win_exe def test_read_pyvenv_cfg(tmpdir): @@ -112,7 +113,7 @@ def test_unhealthy_python_goes_missing(python_dir): python.install_environment(prefix, C.DEFAULT, ()) - exe_name = 'python' if sys.platform != 'win32' else 'python.exe' + exe_name = win_exe('python') py_exe = prefix.path(python.bin_dir('py_env-default'), exe_name) os.remove(py_exe) @@ -158,7 +159,7 @@ def test_unhealthy_then_replaced(python_dir): python.install_environment(prefix, C.DEFAULT, ()) # simulate an exe which returns an old version - exe_name = 'python.exe' if sys.platform == 'win32' else 'python' + exe_name = win_exe('python') py_exe = prefix.path(python.bin_dir('py_env-default'), exe_name) os.rename(py_exe, f'{py_exe}.tmp') diff --git a/tests/repository_test.py b/tests/repository_test.py index af829c2e7..e372519a5 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1043,10 +1043,50 @@ def test_local_perl_additional_dependencies(store): def test_dotnet_hook(tempdir_factory, store, repo): _test_hook_repo( tempdir_factory, store, repo, - 'dotnet example hook', [], b'Hello from dotnet!\n', + 'dotnet-example-hook', [], b'Hello from dotnet!\n', ) +def test_dart_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'dart_repo', + 'hello-world-dart', [], b'hello hello world\n', + ) + + +def test_local_dart_additional_dependencies(store): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'local-dart', + 'name': 'local-dart', + 'entry': 'hello-world-dart', + 'language': 'dart', + 'additional_dependencies': ['hello_world_dart'], + }], + } + hook = _get_hook(config, store, 'local-dart') + ret, out = _hook_run(hook, (), color=False) + assert (ret, _norm_out(out)) == (0, b'hello hello world\n') + + +def test_local_dart_additional_dependencies_versioned(store): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'local-dart', + 'name': 'local-dart', + 'entry': 'secure-random -l 4 -b 16', + 'language': 'dart', + 'additional_dependencies': ['encrypt:5.0.0'], + }], + } + hook = _get_hook(config, store, 'local-dart') + ret, out = _hook_run(hook, (), color=False) + assert ret == 0 + re_assert.Matches('^[a-f0-9]{8}\r?\n$').assert_matches(out.decode()) + + def test_non_installable_hook_error_for_language_version(store, caplog): config = { 'repo': 'local', From 46c18d9370a4610df74b0e3ecb0b4b8694a37991 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 Aug 2021 18:35:12 +0000 Subject: [PATCH 546/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.24.0 → v2.25.0](https://github.com/asottile/pyupgrade/compare/v2.24.0...v2.25.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6dd99d78d..4919da883 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.24.0 + rev: v2.25.0 hooks: - id: pyupgrade args: [--py36-plus] From 54a481c04be1fab75d94d92ba10d858fd521144e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Aug 2021 20:48:41 -0400 Subject: [PATCH 547/967] update tests for latest git --- tests/commands/install_uninstall_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 314b8b969..7076f8269 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -948,7 +948,7 @@ def test_pre_merge_commit_integration(tempdir_factory, store): output_pattern = re_assert.Matches( r'^\[INFO\] Initializing environment for .+\n' r'Bash hook\.+Passed\n' - r"Merge made by the 'recursive' strategy.\n" + r"Merge made by the '(ort|recursive)' strategy.\n" r' foo \| 0\n' r' 1 file changed, 0 insertions\(\+\), 0 deletions\(-\)\n' r' create mode 100644 foo\n$', From 35d3ed40cd0a3545b0d9dea8061de0680b526e73 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Aug 2021 19:58:59 -0400 Subject: [PATCH 548/967] fix check-useless-excludes for exclude of broken symlink --- .../meta_hooks/check_useless_excludes.py | 3 +++ .../meta_hooks/check_useless_excludes_test.py | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 12be03f8a..61165973f 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -43,6 +43,9 @@ def check_useless_excludes(config_file: str) -> int: for repo in config['repos']: for hook in repo['hooks']: + # the default of manifest hooks is `types: [file]` but we may + # be configuring a symlink hook while there's a broken symlink + hook.setdefault('types', []) # Not actually a manifest dict, but this more accurately reflects # the defaults applied during runtime hook = apply_defaults(hook, MANIFEST_HOOK_DICT) diff --git a/tests/meta_hooks/check_useless_excludes_test.py b/tests/meta_hooks/check_useless_excludes_test.py index d261e8142..703bd250c 100644 --- a/tests/meta_hooks/check_useless_excludes_test.py +++ b/tests/meta_hooks/check_useless_excludes_test.py @@ -1,5 +1,10 @@ +from pre_commit import git from pre_commit.meta_hooks import check_useless_excludes +from pre_commit.util import cmd_output from testing.fixtures import add_config_to_repo +from testing.fixtures import make_config_from_repo +from testing.fixtures import make_repo +from testing.util import xfailif_windows def test_useless_exclude_global(capsys, in_git_dir): @@ -113,3 +118,20 @@ def test_valid_exclude(capsys, in_git_dir): out, _ = capsys.readouterr() assert out == '' + + +@xfailif_windows # pragma: win32 no cover +def test_useless_excludes_broken_symlink(capsys, in_git_dir, tempdir_factory): + path = make_repo(tempdir_factory, 'script_hooks_repo') + config = make_config_from_repo(path) + config['hooks'][0]['exclude'] = 'broken-symlink' + add_config_to_repo(in_git_dir.strpath, config) + + in_git_dir.join('broken-symlink').mksymlinkto('DNE') + cmd_output('git', 'add', 'broken-symlink') + git.commit() + + assert check_useless_excludes.main(('.pre-commit-config.yaml',)) == 0 + + out, _ = capsys.readouterr() + assert out == '' From 726f2ad0a33c02441f165f96f907f2855d1b31bb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Aug 2021 20:17:35 -0400 Subject: [PATCH 549/967] remove duplicate warnings while running autoupdate --- pre_commit/commands/migrate_config.py | 4 ---- tests/commands/migrate_config_test.py | 13 ------------- 2 files changed, 17 deletions(-) diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index a155f6b05..fef14cd39 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -3,7 +3,6 @@ import yaml -from pre_commit.clientlib import load_config from pre_commit.util import yaml_load @@ -40,9 +39,6 @@ def _migrate_sha_to_rev(contents: str) -> str: def migrate_config(config_file: str, quiet: bool = False) -> int: - # ensure that the configuration is a valid pre-commit configuration - load_config(config_file) - with open(config_file) as f: orig_contents = contents = f.read() diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index f5c89d044..f5eddd3d6 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -1,7 +1,4 @@ -import pytest - import pre_commit.constants as C -from pre_commit.clientlib import InvalidConfigError from pre_commit.commands.migrate_config import migrate_config @@ -130,13 +127,3 @@ def test_migrate_config_sha_to_rev(tmpdir): ' rev: v1.2.0\n' ' hooks: []\n' ) - - -@pytest.mark.parametrize('contents', ('', '\n')) -def test_migrate_config_invalid_configuration(tmpdir, contents): - cfg = tmpdir.join(C.CONFIG_FILE) - cfg.write(contents) - with tmpdir.as_cwd(), pytest.raises(InvalidConfigError): - migrate_config(C.CONFIG_FILE) - # even though the config is invalid, this should be a noop - assert cfg.read() == contents From 4cd8b364dd871d38e6de891a1a19e52b030e51a9 Mon Sep 17 00:00:00 2001 From: Jordan Speicher Date: Wed, 1 Sep 2021 14:50:59 -0500 Subject: [PATCH 550/967] Add: post-rewrite hook support --- pre_commit/commands/hook_impl.py | 5 ++++ pre_commit/commands/run.py | 7 +++++- pre_commit/constants.py | 1 + pre_commit/main.py | 8 +++++++ testing/util.py | 2 ++ tests/commands/hook_impl_test.py | 9 ++++++++ tests/commands/install_uninstall_test.py | 29 ++++++++++++++++++++++++ tests/commands/run_test.py | 9 ++++++++ tests/repository_test.py | 1 + 9 files changed, 70 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index c544167c1..90bb33b8d 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -78,6 +78,7 @@ def _ns( commit_msg_filename: Optional[str] = None, checkout_type: Optional[str] = None, is_squash_merge: Optional[str] = None, + rewrite_command: Optional[str] = None, ) -> argparse.Namespace: return argparse.Namespace( color=color, @@ -92,6 +93,7 @@ def _ns( all_files=all_files, checkout_type=checkout_type, is_squash_merge=is_squash_merge, + rewrite_command=rewrite_command, files=(), hook=None, verbose=False, @@ -166,6 +168,7 @@ def _pre_push_ns( 'pre-commit': 0, 'pre-merge-commit': 0, 'post-merge': 1, + 'post-rewrite': 1, 'pre-push': 2, } @@ -209,6 +212,8 @@ def _run_ns( ) elif hook_type == 'post-merge': return _ns(hook_type, color, is_squash_merge=args[0]) + elif hook_type == 'post-rewrite': + return _ns(hook_type, color, rewrite_command=args[0]) else: raise AssertionError(f'unexpected hook type: {hook_type}') diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index d906d5b83..95ad5e964 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -245,7 +245,9 @@ def _compute_cols(hooks: Sequence[Hook]) -> int: def _all_filenames(args: argparse.Namespace) -> Collection[str]: # these hooks do not operate on files - if args.hook_stage in {'post-checkout', 'post-commit', 'post-merge'}: + if args.hook_stage in { + 'post-checkout', 'post-commit', 'post-merge', 'post-rewrite', + }: return () elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}: return (args.commit_msg_filename,) @@ -386,6 +388,9 @@ def run( if args.is_squash_merge: environ['PRE_COMMIT_IS_SQUASH_MERGE'] = args.is_squash_merge + if args.rewrite_command: + environ['PRE_COMMIT_REWRITE_COMMAND'] = args.rewrite_command + # Set pre_commit flag environ['PRE_COMMIT'] = '1' diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 3dcbbaca3..1a69c9041 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -19,6 +19,7 @@ STAGES = ( 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', 'post-commit', 'manual', 'post-checkout', 'push', 'post-merge', + 'post-rewrite', ) DEFAULT = 'default' diff --git a/pre_commit/main.py b/pre_commit/main.py index ad3d87370..2b50c91bb 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -69,6 +69,7 @@ def _add_hook_type_option(parser: argparse.ArgumentParser) -> None: '-t', '--hook-type', choices=( 'pre-commit', 'pre-merge-commit', 'pre-push', 'prepare-commit-msg', 'commit-msg', 'post-commit', 'post-checkout', 'post-merge', + 'post-rewrite', ), action=AppendReplaceDefault, default=['pre-commit'], @@ -146,6 +147,13 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: 'squash merge' ), ) + parser.add_argument( + '--rewrite-command', + help=( + 'During a post-rewrite hook, specifies the command that invoked ' + 'the rewrite' + ), + ) def _adjust_args_and_chdir(args: argparse.Namespace) -> None: diff --git a/testing/util.py b/testing/util.py index 12f22b59d..791a2b955 100644 --- a/testing/util.py +++ b/testing/util.py @@ -72,6 +72,7 @@ def run_opts( commit_msg_filename='', checkout_type='', is_squash_merge='', + rewrite_command='', ): # These are mutually exclusive assert not (all_files and files) @@ -92,6 +93,7 @@ def run_opts( commit_msg_filename=commit_msg_filename, checkout_type=checkout_type, is_squash_merge=is_squash_merge, + rewrite_command=rewrite_command, ) diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index c38b9caa1..37b78bc0d 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -99,6 +99,7 @@ def call(*_, **__): ('post-commit', []), ('post-merge', ['1']), ('post-checkout', ['old_head', 'new_head', '1']), + ('post-rewrite', ['amend']), # multiple choices for commit-editmsg ('prepare-commit-msg', ['.git/COMMIT_EDITMSG']), ('prepare-commit-msg', ['.git/COMMIT_EDITMSG', 'message']), @@ -166,6 +167,14 @@ def test_run_ns_post_merge(): assert ns.is_squash_merge == '1' +def test_run_ns_post_rewrite(): + ns = hook_impl._run_ns('post-rewrite', True, ('amend',), b'') + assert ns is not None + assert ns.hook_stage == 'post-rewrite' + assert ns.color is True + assert ns.rewrite_command == 'amend' + + def test_run_ns_post_checkout(): ns = hook_impl._run_ns('post-checkout', True, ('a', 'b', 'c'), b'') assert ns is not None diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 7076f8269..3c0712420 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -817,6 +817,35 @@ def test_post_merge_integration(tempdir_factory, store): assert os.path.exists('post-merge.tmp') +def test_post_rewrite_integration(tempdir_factory, store): + path = git_dir(tempdir_factory) + config = [ + { + 'repo': 'local', + 'hooks': [{ + 'id': 'post-rewrite', + 'name': 'Post rewrite', + 'entry': 'touch post-rewrite.tmp', + 'language': 'system', + 'always_run': True, + 'verbose': True, + 'stages': ['post-rewrite'], + }], + }, + ] + write_config(path, config) + with cwd(path): + open('init', 'a').close() + cmd_output('git', 'add', '.') + install(C.CONFIG_FILE, store, hook_types=['post-rewrite']) + git_commit() + + assert not os.path.exists('post-rewrite.tmp') + + git_commit('--amend', '-m', 'ammended message') + assert os.path.exists('post-rewrite.tmp') + + def test_post_checkout_integration(tempdir_factory, store): path = git_dir(tempdir_factory) config = [ diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index da7569ed0..8c153957b 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -504,6 +504,15 @@ def test_is_squash_merge(cap_out, store, repo_with_passing_hook): assert environ['PRE_COMMIT_IS_SQUASH_MERGE'] == '1' +def test_rewrite_command(cap_out, store, repo_with_passing_hook): + args = run_opts(rewrite_command='amend') + environ: MutableMapping[str, str] = {} + ret, printed = _do_run( + cap_out, store, repo_with_passing_hook, args, environ, + ) + assert environ['PRE_COMMIT_REWRITE_COMMAND'] == 'amend' + + def test_checkout_type(cap_out, store, repo_with_passing_hook): args = run_opts(from_ref='', to_ref='', checkout_type='1') environ: MutableMapping[str, str] = {} diff --git a/tests/repository_test.py b/tests/repository_test.py index e372519a5..4121fed43 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1002,6 +1002,7 @@ def test_manifest_hooks(tempdir_factory, store): stages=( 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', 'post-commit', 'manual', 'post-checkout', 'push', 'post-merge', + 'post-rewrite', ), types=['file'], types_or=[], From 36b8ad63d2d92e8413146e868910964fbf1e46e4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 2 Sep 2021 20:33:19 -0400 Subject: [PATCH 551/967] v2.15.0 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 18 ++++++++++++++++++ setup.cfg | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4919da883..57466c701 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.14.1 + rev: v2.15.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index f77ec9518..6b932566e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +2.15.0 - 2021-09-02 +=================== + +### Features +- add support for hooks written in `dart`. + - #2027 PR by @asottile. +- add support for `post-rewrite` hooks. + - #2036 PR by @uSpike. + - #2035 issue by @uSpike. + +### Fixes +- fix `check-useless-excludes` with exclude matching broken symlink. + - #2029 PR by @asottile. + - #2019 issue by @pkoch. +- eliminate duplicate mutable sha warning messages for `pre-commit autoupdate`. + - #2030 PR by @asottile. + - #2010 issue by @graingert. + 2.14.1 - 2021-08-28 =================== diff --git a/setup.cfg b/setup.cfg index 47fa310f2..c0f4f0eb5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.14.1 +version = 2.15.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 09ffe421a9c1f35934ef1d6f7b14c15c6515381b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 6 Sep 2021 15:01:42 -0400 Subject: [PATCH 552/967] add a bug report issue form --- .github/ISSUE_TEMPLATE/bug.yaml | 41 +++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug.yaml diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml new file mode 100644 index 000000000..6cce5fef7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -0,0 +1,41 @@ +name: bug report +description: something went wrong +body: + - type: markdown + attributes: + value: | + this is for issues for `pre-commit` (the framework). + if you are reporting an issue for [pre-commit.ci] please report it at [pre-commit-ci/issues] + + [pre-commit.ci]: https://pre-commit.ci + [pre-commit-ci/issues]: https://github.com/pre-commit-ci/issues + - type: textarea + id: freeform + attributes: + label: describe your issue + placeholder: 'I was doing ... I ran ... I expected ... I got ...' + validations: + required: true + - type: input + id: version + attributes: + label: pre-commit --version + placeholder: pre-commit x.x.x + validations: + required: true + - type: textarea + id: configuration + attributes: + label: .pre-commit-config.yaml + description: (auto-rendered as yaml, no need for backticks) + placeholder: 'repos: ...' + render: yaml + validations: + required: true + - type: textarea + id: error-log + attributes: + label: '~/.cache/pre-commit/pre-commit.log (if present)' + placeholder: "### version information\n..." + validations: + required: false From ab94dd69f8408e95c6a49a0e3aa21eeaf39d3f1c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 13 Sep 2021 20:01:25 -0400 Subject: [PATCH 553/967] fix pre-commit autoupdate for core.useBuiltinFSMonitor=true on windows --- pre_commit/commands/autoupdate.py | 22 +++++++++++++++++----- pre_commit/git.py | 9 ++++++--- tests/commands/autoupdate_test.py | 9 +++++++++ 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 33a347302..5cb978e92 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -36,24 +36,36 @@ def from_config(cls, config: Dict[str, Any]) -> 'RevInfo': return cls(config['repo'], config['rev'], None) def update(self, tags_only: bool, freeze: bool) -> 'RevInfo': + git_cmd = ('git', *git.NO_FS_MONITOR) + if tags_only: - tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags', '--abbrev=0') + tag_cmd = ( + *git_cmd, 'describe', + 'FETCH_HEAD', '--tags', '--abbrev=0', + ) else: - tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags', '--exact') + tag_cmd = ( + *git_cmd, 'describe', + 'FETCH_HEAD', '--tags', '--exact', + ) with tmpdir() as tmp: git.init_repo(tmp, self.repo) - cmd_output_b('git', 'fetch', 'origin', 'HEAD', '--tags', cwd=tmp) + cmd_output_b( + *git_cmd, 'fetch', 'origin', 'HEAD', '--tags', + cwd=tmp, + ) try: rev = cmd_output(*tag_cmd, cwd=tmp)[1].strip() except CalledProcessError: - cmd = ('git', 'rev-parse', 'FETCH_HEAD') + cmd = (*git_cmd, 'rev-parse', 'FETCH_HEAD') rev = cmd_output(*cmd, cwd=tmp)[1].strip() frozen = None if freeze: - exact = cmd_output('git', 'rev-parse', rev, cwd=tmp)[1].strip() + exact_rev_cmd = (*git_cmd, 'rev-parse', rev) + exact = cmd_output(*exact_rev_cmd, cwd=tmp)[1].strip() if exact != rev: rev, frozen = exact, rev return self._replace(rev=rev, frozen=frozen) diff --git a/pre_commit/git.py b/pre_commit/git.py index 6264529d5..883723ea1 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -12,9 +12,11 @@ from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b - logger = logging.getLogger(__name__) +# see #2046 +NO_FS_MONITOR = ('-c', 'core.useBuiltinFSMonitor=false') + def zsplit(s: str) -> List[str]: s = s.strip('\0') @@ -185,10 +187,11 @@ def init_repo(path: str, remote: str) -> None: if os.path.isdir(remote): remote = os.path.abspath(remote) + git = ('git', *NO_FS_MONITOR) env = no_git_env() # avoid the user's template so that hooks do not recurse - cmd_output_b('git', 'init', '--template=', path, env=env) - cmd_output_b('git', 'remote', 'add', 'origin', remote, cwd=path, env=env) + cmd_output_b(*git, 'init', '--template=', path, env=env) + cmd_output_b(*git, 'remote', 'add', 'origin', remote, cwd=path, env=env) def commit(repo: str = '.') -> None: diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index b2bad6014..7316eb97a 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -5,6 +5,7 @@ import yaml import pre_commit.constants as C +from pre_commit import envcontext from pre_commit import git from pre_commit import util from pre_commit.commands.autoupdate import _check_hooks_still_exist_at_rev @@ -176,6 +177,14 @@ def test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store): assert cfg.read() == fmt.format(out_of_date.path, out_of_date.head_rev) +def test_autoupdate_with_core_useBuiltinFSMonitor(out_of_date, tmpdir, store): + # force the setting on "globally" for git + home = tmpdir.join('fakehome').ensure_dir() + home.join('.gitconfig').write('[core]\nuseBuiltinFSMonitor = true\n') + with envcontext.envcontext((('HOME', str(home)),)): + test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store) + + def test_autoupdate_pure_yaml(out_of_date, tmpdir, store): with mock.patch.object(util, 'Dumper', yaml.SafeDumper): test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store) From cef9c4af03e954e5fe3e8746a49ac59e4781747d Mon Sep 17 00:00:00 2001 From: Radek SPRTA Date: Fri, 17 Sep 2021 21:16:11 +0200 Subject: [PATCH 554/967] Add warning for regular expression with [\/] (#2043) --- pre_commit/clientlib.py | 24 ++++++++++++++++ tests/clientlib_test.py | 64 +++++++++++++++++++++++++++++------------ 2 files changed, 69 insertions(+), 19 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index bc7274a7d..7d87ee043 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -143,6 +143,18 @@ def check(self, dct: Dict[str, Any]) -> None: f"regex, not a glob -- matching '/*' probably isn't what you " f'want here', ) + if r'[\/]' in dct.get(self.key, ''): + logger.warning( + fr'pre-commit normalizes slashes in the {self.key!r} field ' + fr'in hook {dct.get("id")!r} to forward slashes, so you ' + fr'can use / instead of [\/]', + ) + if r'[/\\]' in dct.get(self.key, ''): + logger.warning( + fr'pre-commit normalizes slashes in the {self.key!r} field ' + fr'in hook {dct.get("id")!r} to forward slashes, so you ' + fr'can use / instead of [/\\]', + ) class OptionalSensibleRegexAtTop(cfgv.OptionalNoDefault): @@ -154,6 +166,18 @@ def check(self, dct: Dict[str, Any]) -> None: f'The top-level {self.key!r} field is a regex, not a glob -- ' f"matching '/*' probably isn't what you want here", ) + if r'[\/]' in dct.get(self.key, ''): + logger.warning( + fr'pre-commit normalizes the slashes in the top-level ' + fr'{self.key!r} field to forward slashes, so you can use / ' + fr'instead of [\/]', + ) + if r'[/\\]' in dct.get(self.key, ''): + logger.warning( + fr'pre-commit normalizes the slashes in the top-level ' + fr'{self.key!r} field to forward slashes, so you can use / ' + fr'instead of [/\\]', + ) class MigrateShaToRev: diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index da794e6e7..5427b1dae 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -247,38 +247,64 @@ def test_warn_mutable_rev_conditional(): cfgv.validate(config_obj, CONFIG_REPO_DICT) -def test_validate_optional_sensible_regex_at_hook_level(caplog): +@pytest.mark.parametrize( + ('regex', 'warning'), + ( + ( + r'dir/*.py', + "The 'files' field in hook 'flake8' is a regex, not a glob -- " + "matching '/*' probably isn't what you want here", + ), + ( + r'dir[\/].*\.py', + r"pre-commit normalizes slashes in the 'files' field in hook " + r"'flake8' to forward slashes, so you can use / instead of [\/]", + ), + ( + r'dir[/\\].*\.py', + r"pre-commit normalizes slashes in the 'files' field in hook " + r"'flake8' to forward slashes, so you can use / instead of [/\\]", + ), + ), +) +def test_validate_optional_sensible_regex_at_hook(caplog, regex, warning): config_obj = { 'id': 'flake8', - 'files': 'dir/*.py', + 'files': regex, } cfgv.validate(config_obj, CONFIG_HOOK_DICT) - assert caplog.record_tuples == [ + assert caplog.record_tuples == [('pre_commit', logging.WARNING, warning)] + + +@pytest.mark.parametrize( + ('regex', 'warning'), + ( ( - 'pre_commit', - logging.WARNING, - "The 'files' field in hook 'flake8' is a regex, not a glob -- " + r'dir/*.py', + "The top-level 'files' field is a regex, not a glob -- " "matching '/*' probably isn't what you want here", ), - ] - - -def test_validate_optional_sensible_regex_at_top_level(caplog): + ( + r'dir[\/].*\.py', + r"pre-commit normalizes the slashes in the top-level 'files' " + r'field to forward slashes, so you can use / instead of [\/]', + ), + ( + r'dir[/\\].*\.py', + r"pre-commit normalizes the slashes in the top-level 'files' " + r'field to forward slashes, so you can use / instead of [/\\]', + ), + ), +) +def test_validate_optional_sensible_regex_at_top_level(caplog, regex, warning): config_obj = { - 'files': 'dir/*.py', + 'files': regex, 'repos': [], } cfgv.validate(config_obj, CONFIG_SCHEMA) - assert caplog.record_tuples == [ - ( - 'pre_commit', - logging.WARNING, - "The top-level 'files' field is a regex, not a glob -- matching " - "'/*' probably isn't what you want here", - ), - ] + assert caplog.record_tuples == [('pre_commit', logging.WARNING, warning)] @pytest.mark.parametrize('fn', (validate_config_main, validate_manifest_main)) From e622f793c3de2276d6975358c7e35e9b890fbdfd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 27 Sep 2021 19:34:04 -0400 Subject: [PATCH 555/967] port hook template to bash this avoids some version-specific code in python this also makes the bootstrap script slightly more portable --- pre_commit/commands/install_uninstall.py | 13 +++--- pre_commit/resources/hook-tmpl | 50 ++++++------------------ tests/commands/install_uninstall_test.py | 6 +-- 3 files changed, 20 insertions(+), 49 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 73c8d6056..7974423b6 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -1,6 +1,7 @@ import itertools import logging import os.path +import shlex import shutil import sys from typing import Optional @@ -100,19 +101,17 @@ def _install_hook_script( args = ['hook-impl', f'--config={config_file}', f'--hook-type={hook_type}'] if skip_on_missing_config: args.append('--skip-on-missing-config') - params = {'INSTALL_PYTHON': sys.executable, 'ARGS': args} with open(hook_path, 'w') as hook_file: contents = resource_text('hook-tmpl') before, rest = contents.split(TEMPLATE_START) - to_template, after = rest.split(TEMPLATE_END) - - before = before.replace('#!/usr/bin/env python3', shebang()) + _, after = rest.split(TEMPLATE_END) hook_file.write(before + TEMPLATE_START) - for line in to_template.splitlines(): - var = line.split()[0] - hook_file.write(f'{var} = {params[var]!r}\n') + hook_file.write(f'INSTALL_PYTHON={shlex.quote(sys.executable)}\n') + # TODO: python3.8+: shlex.join + args_s = ' '.join(shlex.quote(part) for part in args) + hook_file.write(f'ARGS=({args_s})\n') hook_file.write(TEMPLATE_END + after) make_executable(hook_path) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 299144ec7..1dd66a2ae 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -1,44 +1,20 @@ -#!/usr/bin/env python3 +#!/usr/bin/env bash # File generated by pre-commit: https://pre-commit.com # ID: 138fd403232d2ddd5efb44317e38bf03 -import os -import sys - -# we try our best, but the shebang of this script is difficult to determine: -# - macos doesn't ship with python3 -# - windows executables are almost always `python.exe` -# therefore we continue to support python2 for this small script -if sys.version_info < (3, 3): - from distutils.spawn import find_executable as which -else: - from shutil import which - -# work around https://github.com/Homebrew/homebrew-core/issues/30445 -os.environ.pop('__PYVENV_LAUNCHER__', None) # start templated -INSTALL_PYTHON = '' -ARGS = ['hook-impl'] +INSTALL_PYTHON='' +ARGS=(hook-impl) # end templated -ARGS.extend(('--hook-dir', os.path.realpath(os.path.dirname(__file__)))) -ARGS.append('--') -ARGS.extend(sys.argv[1:]) - -DNE = '`pre-commit` not found. Did you forget to activate your virtualenv?' -if os.access(INSTALL_PYTHON, os.X_OK): - CMD = [INSTALL_PYTHON, '-mpre_commit'] -elif which('pre-commit'): - CMD = ['pre-commit'] -else: - raise SystemExit(DNE) -CMD.extend(ARGS) -if sys.platform == 'win32': # https://bugs.python.org/issue19124 - import subprocess +HERE="$(cd "$(dirname "$0")" && pwd)" +ARGS+=(--hook-dir "$HERE" -- "$@") - if sys.version_info < (3, 7): # https://bugs.python.org/issue25942 - raise SystemExit(subprocess.Popen(CMD).wait()) - else: - raise SystemExit(subprocess.call(CMD)) -else: - os.execvp(CMD[0], CMD) +if [ -x "$INSTALL_PYTHON" ]; then + exec "$INSTALL_PYTHON" -mpre_commit "${ARGS[@]}" +elif command -v pre-commit; then + exec pre-commit "${ARGS[@]}" +else + echo '`pre-commit` not found. Did you forget to activate your virtualenv?' 1>&2 + exit 1 +fi diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 3c0712420..833990349 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -278,11 +278,7 @@ def test_environment_not_sourced(tempdir_factory, store): hook = os.path.join(path, '.git/hooks/pre-commit') with open(hook) as f: src = f.read() - src = re.sub( - '\nINSTALL_PYTHON =.*\n', - '\nINSTALL_PYTHON = "/dne"\n', - src, - ) + src = re.sub('\nINSTALL_PYTHON=.*\n', '\nINSTALL_PYTHON="/dne"\n', src) with open(hook, 'w') as f: f.write(src) From 2fc00f73b640fa1ef149a38260b616f89d32a344 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 27 Sep 2021 19:59:58 -0400 Subject: [PATCH 556/967] un-xfail node on windows these have been reasonably stable for a while --- tests/repository_test.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/repository_test.py b/tests/repository_test.py index 4121fed43..6f4047c38 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -245,7 +245,6 @@ def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): ) -@xfailif_windows # pragma: win32 no cover def test_run_a_node_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'node_hooks_repo', @@ -253,7 +252,6 @@ def test_run_a_node_hook(tempdir_factory, store): ) -@xfailif_windows # pragma: win32 no cover def test_run_a_node_hook_default_version(tempdir_factory, store): # make sure that this continues to work for platforms where node is not # installed at the system @@ -263,7 +261,6 @@ def test_run_a_node_hook_default_version(tempdir_factory, store): test_run_a_node_hook(tempdir_factory, store) -@xfailif_windows # pragma: win32 no cover def test_run_versioned_node_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'node_versioned_hooks_repo', @@ -271,7 +268,6 @@ def test_run_versioned_node_hook(tempdir_factory, store): ) -@xfailif_windows # pragma: win32 no cover def test_node_hook_with_npm_userconfig_set(tempdir_factory, store, tmpdir): cfg = tmpdir.join('cfg') cfg.write('cache=/dne\n') @@ -653,7 +649,6 @@ def test_additional_ruby_dependencies_installed(tempdir_factory, store): assert 'tins' in output -@xfailif_windows # pragma: win32 no cover def test_additional_node_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'node_hooks_repo') config = make_config_from_repo(path) From e9ed248a15b89faeb0389877b02d1f78ffe24243 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 1 Oct 2021 18:45:36 -0400 Subject: [PATCH 557/967] make sure to not discard changes even if submodule.recurse=1 --- pre_commit/staged_files_only.py | 10 ++++++-- tests/staged_files_only_test.py | 43 +++++++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 48cc10299..bad004cd1 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -13,6 +13,12 @@ logger = logging.getLogger('pre_commit') +# without forcing submodule.recurse=0, changes in nested submodules will be +# discarded if `submodule.recurse=1` is configured +# we choose this instead of `--no-recurse-submodules` because it works on +# versions of git before that option was added to `git checkout` +_CHECKOUT_CMD = ('git', '-c', 'submodule.recurse=0', 'checkout', '--', '.') + def _git_apply(patch: str) -> None: args = ('apply', '--whitespace=nowarn', patch) @@ -58,7 +64,7 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: # prevent recursive post-checkout hooks (#1418) no_checkout_env = dict(os.environ, _PRE_COMMIT_SKIP_POST_CHECKOUT='1') - cmd_output_b('git', 'checkout', '--', '.', env=no_checkout_env) + cmd_output_b(*_CHECKOUT_CMD, env=no_checkout_env) try: yield @@ -74,7 +80,7 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: # We failed to apply the patch, presumably due to fixes made # by hooks. # Roll back the changes made by hooks. - cmd_output_b('git', 'checkout', '--', '.', env=no_checkout_env) + cmd_output_b(*_CHECKOUT_CMD, env=no_checkout_env) _git_apply(patch_filename) logger.info(f'Restored changes from {patch_filename}.') diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index ddb957435..2e3f62091 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -181,9 +181,11 @@ def test_img_conflict(img_staged, patch_dir): @pytest.fixture -def submodule_with_commits(tempdir_factory): +def repo_with_commits(tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): + open('foo', 'a+').close() + cmd_output('git', 'add', 'foo') git_commit() rev1 = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() git_commit() @@ -196,18 +198,21 @@ def checkout_submodule(rev): @pytest.fixture -def sub_staged(submodule_with_commits, tempdir_factory): +def sub_staged(repo_with_commits, tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): + open('bar', 'a+').close() + cmd_output('git', 'add', 'bar') + git_commit() cmd_output( - 'git', 'submodule', 'add', submodule_with_commits.path, 'sub', + 'git', 'submodule', 'add', repo_with_commits.path, 'sub', ) - checkout_submodule(submodule_with_commits.rev1) + checkout_submodule(repo_with_commits.rev1) cmd_output('git', 'add', 'sub') yield auto_namedtuple( path=path, sub_path=os.path.join(path, 'sub'), - submodule=submodule_with_commits, + submodule=repo_with_commits, ) @@ -242,6 +247,34 @@ def test_sub_something_unstaged(sub_staged, patch_dir): _test_sub_state(sub_staged, 'rev2', 'AM') +def test_submodule_does_not_discard_changes(sub_staged, patch_dir): + with open('bar', 'w') as f: + f.write('unstaged changes') + + foo_path = os.path.join(sub_staged.sub_path, 'foo') + with open(foo_path, 'w') as f: + f.write('foo contents') + + with staged_files_only(patch_dir): + with open('bar') as f: + assert f.read() == '' + + with open(foo_path) as f: + assert f.read() == 'foo contents' + + with open('bar') as f: + assert f.read() == 'unstaged changes' + + with open(foo_path) as f: + assert f.read() == 'foo contents' + + +def test_submodule_does_not_discard_changes_recurse(sub_staged, patch_dir): + cmd_output('git', 'config', 'submodule.recurse', '1', cwd=sub_staged.path) + + test_submodule_does_not_discard_changes(sub_staged, patch_dir) + + def test_stage_utf8_changes(foo_staged, patch_dir): contents = '\u2603' with open('foo', 'w', encoding='UTF-8') as foo_file: From 0acf2e99c4e81757beff37cf399d066a57ae1ddf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Oct 2021 18:54:25 +0000 Subject: [PATCH 558/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.25.0 → v2.29.0](https://github.com/asottile/pyupgrade/compare/v2.25.0...v2.29.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 57466c701..eb1995b56 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.25.0 + rev: v2.29.0 hooks: - id: pyupgrade args: [--py36-plus] From 247d892e69007de18d7b0c3fdd36056b50f43d02 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 18:53:36 +0000 Subject: [PATCH 559/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 3.9.2 → 4.0.1](https://github.com/PyCQA/flake8/compare/3.9.2...4.0.1) - [github.com/asottile/setup-cfg-fmt: v1.17.0 → v1.18.0](https://github.com/asottile/setup-cfg-fmt/compare/v1.17.0...v1.18.0) - [github.com/pre-commit/mirrors-mypy: v0.910 → v0.910-1](https://github.com/pre-commit/mirrors-mypy/compare/v0.910...v0.910-1) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eb1995b56..f635586b9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: - id: requirements-txt-fixer - id: double-quote-string-fixer - repo: https://github.com/PyCQA/flake8 - rev: 3.9.2 + rev: 4.0.1 hooks: - id: flake8 additional_dependencies: [flake8-typing-imports==1.10.0] @@ -40,11 +40,11 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.17.0 + rev: v1.18.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.910 + rev: v0.910-1 hooks: - id: mypy additional_dependencies: [types-all] From 69a4dbda68d3db74ad217277dac653ab68404bf4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 18:53:57 +0000 Subject: [PATCH 560/967] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index c0f4f0eb5..26832b82a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,6 +17,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy From 8c844c794d97a7be75c57f5b8d7df2a7674e768f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 11 Oct 2021 20:20:30 -0400 Subject: [PATCH 561/967] work around conda bug installing python3.1/site-packages https://github.com/conda/conda/issues/10969 --- tests/repository_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/repository_test.py b/tests/repository_test.py index 6f4047c38..5f259070e 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -111,8 +111,8 @@ def test_local_conda_additional_dependencies(store): 'name': 'local-conda', 'entry': 'python', 'language': 'conda', - 'args': ['-c', 'import tzdata; print("OK")'], - 'additional_dependencies': ['python-tzdata'], + 'args': ['-c', 'import botocore; print("OK")'], + 'additional_dependencies': ['botocore'], }], } hook = _get_hook(config, store, 'local-conda') From d0c9e589ca41d8b2a072e518bbaecb0df28e33d6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 19 Oct 2021 19:02:36 -0700 Subject: [PATCH 562/967] ban broken importlib-resources versions --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 26832b82a..975dd77b8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,7 +31,7 @@ install_requires = toml virtualenv>=20.0.8 importlib-metadata;python_version<"3.8" - importlib-resources;python_version<"3.7" + importlib-resources<5.3;python_version<"3.7" python_requires = >=3.6.1 [options.packages.find] From 63ae399db0b220b0e59b7055b98391b6851c2246 Mon Sep 17 00:00:00 2001 From: Stojan Nedic Date: Tue, 19 Oct 2021 22:17:42 +0200 Subject: [PATCH 563/967] Add fail_fast support per-hook --- pre_commit/clientlib.py | 1 + pre_commit/commands/run.py | 2 +- pre_commit/hook.py | 1 + tests/commands/run_test.py | 12 ++++++++++++ tests/repository_test.py | 1 + 5 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 7d87ee043..6377a8b67 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -70,6 +70,7 @@ def _make_argparser(filenames_help: str) -> argparse.ArgumentParser: ), cfgv.Optional('args', cfgv.check_array(cfgv.check_string), []), cfgv.Optional('always_run', cfgv.check_bool, False), + cfgv.Optional('fail_fast', cfgv.check_bool, False), cfgv.Optional('pass_filenames', cfgv.check_bool, True), cfgv.Optional('description', cfgv.check_string, ''), cfgv.Optional('language_version', cfgv.check_string, C.DEFAULT), diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 95ad5e964..2714faf40 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -290,7 +290,7 @@ def _run_hooks( verbose=args.verbose, use_color=args.color, ) retval |= current_retval - if retval and config['fail_fast']: + if retval and (config['fail_fast'] or hook.fail_fast): break if retval and args.show_diff_on_failure and prior_diff: if args.all_files: diff --git a/pre_commit/hook.py b/pre_commit/hook.py index ea773942b..82e99c543 100644 --- a/pre_commit/hook.py +++ b/pre_commit/hook.py @@ -27,6 +27,7 @@ class Hook(NamedTuple): additional_dependencies: Sequence[str] args: Sequence[str] always_run: bool + fail_fast: bool pass_filenames: bool description: str language_version: str diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 8c153957b..3a6fa2a10 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -985,6 +985,18 @@ def test_fail_fast(cap_out, store, repo_with_failing_hook): assert printed.count(b'Failing hook') == 1 +def test_fail_fast_per_hook(cap_out, store, repo_with_failing_hook): + with modify_config() as config: + # More than one hook + config['repos'][0]['hooks'] *= 2 + config['repos'][0]['hooks'][0]['fail_fast'] = True + stage_a_file() + + ret, printed = _do_run(cap_out, store, repo_with_failing_hook, run_opts()) + # it should have only run one hook + assert printed.count(b'Failing hook') == 1 + + def test_classifier_removes_dne(): classifier = Classifier(('this_file_does_not_exist',)) assert classifier.filenames == [] diff --git a/tests/repository_test.py b/tests/repository_test.py index 5f259070e..96c54e834 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1002,6 +1002,7 @@ def test_manifest_hooks(tempdir_factory, store): types=['file'], types_or=[], verbose=False, + fail_fast=False, ) From c8cf74dc718ee42cb6db8c0ac7dccc25b219474d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 23 Oct 2021 13:23:48 -0400 Subject: [PATCH 564/967] replace exit(main()) with raise SystemExit(main()) Committed via https://github.com/asottile/all-repos --- pre_commit/__main__.py | 2 +- pre_commit/languages/pygrep.py | 2 +- pre_commit/main.py | 2 +- pre_commit/meta_hooks/check_hooks_apply.py | 2 +- pre_commit/meta_hooks/check_useless_excludes.py | 2 +- pre_commit/meta_hooks/identity.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pre_commit/__main__.py b/pre_commit/__main__.py index 541406879..83bd93ca4 100644 --- a/pre_commit/__main__.py +++ b/pre_commit/__main__.py @@ -2,4 +2,4 @@ if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index c80d6794b..a713c3fb5 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -124,4 +124,4 @@ def main(argv: Optional[Sequence[str]] = None) -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/pre_commit/main.py b/pre_commit/main.py index 2b50c91bb..f1e8d03db 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -411,4 +411,4 @@ def main(argv: Optional[Sequence[str]] = None) -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index a1e93529a..a6eb0e094 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -39,4 +39,4 @@ def main(argv: Optional[Sequence[str]] = None) -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 61165973f..60870f835 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -77,4 +77,4 @@ def main(argv: Optional[Sequence[str]] = None) -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/pre_commit/meta_hooks/identity.py b/pre_commit/meta_hooks/identity.py index 730d0ec00..12eb02f92 100644 --- a/pre_commit/meta_hooks/identity.py +++ b/pre_commit/meta_hooks/identity.py @@ -13,4 +13,4 @@ def main(argv: Optional[Sequence[str]] = None) -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) From 28cafc2273c4d331af1736151007833daa299749 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 24 Oct 2021 07:19:57 -0700 Subject: [PATCH 565/967] exit(main()) -> raise SystemExit(main()) pt2 Committed via https://github.com/asottile/all-repos --- testing/gen-languages-all | 2 +- testing/make-archives | 2 +- testing/zipapp/entry | 2 +- testing/zipapp/make | 2 +- testing/zipapp/python | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/testing/gen-languages-all b/testing/gen-languages-all index 51e4bce62..c933c1435 100755 --- a/testing/gen-languages-all +++ b/testing/gen-languages-all @@ -25,4 +25,4 @@ def main() -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/testing/make-archives b/testing/make-archives index cb0b0a408..707fd8843 100755 --- a/testing/make-archives +++ b/testing/make-archives @@ -80,4 +80,4 @@ def main(argv: Optional[Sequence[str]] = None) -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/testing/zipapp/entry b/testing/zipapp/entry index f0a345e6a..87f9291dc 100755 --- a/testing/zipapp/entry +++ b/testing/zipapp/entry @@ -68,4 +68,4 @@ def main() -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/testing/zipapp/make b/testing/zipapp/make index 8740b2f5a..55b6d2c7e 100755 --- a/testing/zipapp/make +++ b/testing/zipapp/make @@ -106,4 +106,4 @@ def main() -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/testing/zipapp/python b/testing/zipapp/python index 97c5928e3..7184a1aae 100755 --- a/testing/zipapp/python +++ b/testing/zipapp/python @@ -45,4 +45,4 @@ def main() -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) From 2b30fbcfd582d2685c401fa05cab3621f6fdf17b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Oct 2021 18:59:17 +0000 Subject: [PATCH 566/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/add-trailing-comma: v2.1.0 → v2.2.0](https://github.com/asottile/add-trailing-comma/compare/v2.1.0...v2.2.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f635586b9..0ea04f704 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: - id: reorder-python-imports args: [--py3-plus] - repo: https://github.com/asottile/add-trailing-comma - rev: v2.1.0 + rev: v2.2.0 hooks: - id: add-trailing-comma args: [--py36-plus] From 0b87867729501287937c633889a677437630353b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 28 Oct 2021 21:21:59 -0700 Subject: [PATCH 567/967] silence the output of `command -v` --- pre_commit/resources/hook-tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 1dd66a2ae..4ebeb2e1d 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -12,7 +12,7 @@ ARGS+=(--hook-dir "$HERE" -- "$@") if [ -x "$INSTALL_PYTHON" ]; then exec "$INSTALL_PYTHON" -mpre_commit "${ARGS[@]}" -elif command -v pre-commit; then +elif command -v pre-commit > /dev/null; then exec pre-commit "${ARGS[@]}" else echo '`pre-commit` not found. Did you forget to activate your virtualenv?' 1>&2 From 087541cb2d7ec46e5271df53eb6edf747619e720 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 30 Oct 2021 12:11:52 -0400 Subject: [PATCH 568/967] fix indent in hook-tmpl --- pre_commit/resources/hook-tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 4ebeb2e1d..53d29f954 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -15,6 +15,6 @@ if [ -x "$INSTALL_PYTHON" ]; then elif command -v pre-commit > /dev/null; then exec pre-commit "${ARGS[@]}" else - echo '`pre-commit` not found. Did you forget to activate your virtualenv?' 1>&2 - exit 1 + echo '`pre-commit` not found. Did you forget to activate your virtualenv?' 1>&2 + exit 1 fi From 141e18319a8863ed495e419e3d6d8731e98434ce Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 19:35:28 +0000 Subject: [PATCH 569/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v1.18.0 → v1.19.0](https://github.com/asottile/setup-cfg-fmt/compare/v1.18.0...v1.19.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0ea04f704..98571aaa5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.18.0 + rev: v1.19.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy From b2a35414aaadf9c1bd4b5770ed3afe445a7d4aa6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 22 Nov 2021 18:41:26 -0500 Subject: [PATCH 570/967] bump perltidy version --- tests/repository_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/repository_test.py b/tests/repository_test.py index 96c54e834..c787eb020 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1021,13 +1021,13 @@ def test_local_perl_additional_dependencies(store): 'name': 'hello', 'entry': 'perltidy --version', 'language': 'perl', - 'additional_dependencies': ['SHANCOCK/Perl-Tidy-20200110.tar.gz'], + 'additional_dependencies': ['SHANCOCK/Perl-Tidy-20211029.tar.gz'], }], } hook = _get_hook(config, store, 'hello') ret, out = _hook_run(hook, (), color=False) assert ret == 0 - assert _norm_out(out).startswith(b'This is perltidy, v20200110') + assert _norm_out(out).startswith(b'This is perltidy, v20211029') @pytest.mark.parametrize( From a064f248d7a48980393bb76d2d82eef55319df74 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Nov 2021 19:51:34 +0000 Subject: [PATCH 571/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.29.0 → v2.29.1](https://github.com/asottile/pyupgrade/compare/v2.29.0...v2.29.1) - [github.com/asottile/add-trailing-comma: v2.2.0 → v2.2.1](https://github.com/asottile/add-trailing-comma/compare/v2.2.0...v2.2.1) - [github.com/asottile/setup-cfg-fmt: v1.19.0 → v1.20.0](https://github.com/asottile/setup-cfg-fmt/compare/v1.19.0...v1.20.0) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 98571aaa5..2bdfee071 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.29.0 + rev: v2.29.1 hooks: - id: pyupgrade args: [--py36-plus] @@ -35,12 +35,12 @@ repos: - id: reorder-python-imports args: [--py3-plus] - repo: https://github.com/asottile/add-trailing-comma - rev: v2.2.0 + rev: v2.2.1 hooks: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.19.0 + rev: v1.20.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy From 4eb91cdd8e45f968f70605c3a1568016a0d2d0e1 Mon Sep 17 00:00:00 2001 From: Marius Zwicker Date: Mon, 22 Nov 2021 21:54:58 +0100 Subject: [PATCH 572/967] support gitconfig from env Add exceptions to the git env so externally configured gitconfig values set via GIT_CONFIG_KEY_, GIT_CONFIG_VALUE_ and GIT_CONFIG_COUNT get passed through. --- pre_commit/git.py | 3 ++- tests/git_test.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 883723ea1..e9ec60140 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -41,9 +41,10 @@ def no_git_env( return { k: v for k, v in _env.items() if not k.startswith('GIT_') or + k.startswith(('GIT_CONFIG_KEY_', 'GIT_CONFIG_VALUE_')) or k in { 'GIT_EXEC_PATH', 'GIT_SSH', 'GIT_SSH_COMMAND', 'GIT_SSL_CAINFO', - 'GIT_SSL_NO_VERIFY', + 'GIT_SSL_NO_VERIFY', 'GIT_CONFIG_COUNT', } } diff --git a/tests/git_test.py b/tests/git_test.py index aa218804c..bcb3fd15b 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -227,6 +227,11 @@ def test_no_git_env(): 'GIT_SSH': '/usr/bin/ssh', 'GIT_SSH_COMMAND': 'ssh -o', 'GIT_DIR': '/none/shall/pass', + 'GIT_CONFIG_KEY_0': 'user.name', + 'GIT_CONFIG_VALUE_0': 'anthony', + 'GIT_CONFIG_KEY_1': 'user.email', + 'GIT_CONFIG_VALUE_1': 'asottile@example.com', + 'GIT_CONFIG_COUNT': '2', } no_git_env = git.no_git_env(env) assert no_git_env == { @@ -234,6 +239,11 @@ def test_no_git_env(): 'GIT_EXEC_PATH': '/some/git/exec/path', 'GIT_SSH': '/usr/bin/ssh', 'GIT_SSH_COMMAND': 'ssh -o', + 'GIT_CONFIG_KEY_0': 'user.name', + 'GIT_CONFIG_VALUE_0': 'anthony', + 'GIT_CONFIG_KEY_1': 'user.email', + 'GIT_CONFIG_VALUE_1': 'asottile@example.com', + 'GIT_CONFIG_COUNT': '2', } From c45b84bd3914866711c4af01ce14870035a1aadc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 23 Nov 2021 11:24:26 -0500 Subject: [PATCH 573/967] Use org-default .github/FUNDING.yml Committed via https://github.com/asottile/all-repos --- .github/FUNDING.yml | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 9408e44d6..000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,2 +0,0 @@ -github: asottile -open_collective: pre-commit From 270b539e8f1da3bf8a05ffc90a2f67952639f0f6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 29 Nov 2021 20:45:40 -0500 Subject: [PATCH 574/967] improve coverage pragmas with covdefaults 2.1 Committed via https://github.com/asottile/all-repos --- pre_commit/constants.py | 6 +++--- pre_commit/util.py | 4 ++-- requirements-dev.txt | 2 +- tests/repository_test.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 1a69c9041..d2f93636a 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -1,9 +1,9 @@ import sys -if sys.version_info < (3, 8): # pragma: no cover (= (3, 8): # pragma: >=3.8 cover import importlib.metadata as importlib_metadata +else: # pragma: <3.8 cover + import importlib_metadata CONFIG_FILE = '.pre-commit-config.yaml' MANIFEST_FILE = '.pre-commit-hooks.yaml' diff --git a/pre_commit/util.py b/pre_commit/util.py index 6bf8ae7a1..6977acb2c 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -21,10 +21,10 @@ from pre_commit import parse_shebang -if sys.version_info >= (3, 7): # pragma: no cover (PY37+) +if sys.version_info >= (3, 7): # pragma: >=3.7 cover from importlib.resources import open_binary from importlib.resources import read_text -else: # pragma: no cover (=2.1 coverage distlib pytest diff --git a/tests/repository_test.py b/tests/repository_test.py index c787eb020..36268e1e8 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -164,7 +164,7 @@ def test_python_hook_weird_setup_cfg(in_git_dir, tempdir_factory, store): ) -def test_python_venv(tempdir_factory, store): # pragma: no cover (no venv) +def test_python_venv(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'python_venv_hooks_repo', 'foo', [os.devnull], From d91a4c47f33788827e97888af50a893ee5fb79a8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 30 Nov 2021 18:16:47 -0500 Subject: [PATCH 575/967] v2.16.0 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ setup.cfg | 2 +- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2bdfee071..49517c341 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.15.0 + rev: v2.16.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b932566e..55f46d9a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ +2.16.0 - 2021-11-30 +=================== + +### Features +- add warning for regexes containing `[\/]` or `[/\\]`. + - #2053 PR by @radek-sprta. + - #2043 issue by @asottile. +- move hook template back to `bash` resolving shebang-portability issues. + - #2065 PR by @asottile. +- add support for `fail_fast` at the individual hook level. + - #2097 PR by @colens3. + - #1143 issue by @potiuk. +- allow passthrough of `GIT_CONFIG_KEY_*`, `GIT_CONFIG_VALUE_*`, and + `GIT_CONFIG_COUNT`. + - #2136 PR by @emzeat. + +### Fixes +- fix pre-commit autoupdate for `core.useBuiltinFSMonitor=true` on windows. + - #2047 PR by @asottile. + - #2046 issue by @lcnittl. +- fix temporary file stashing with for `submodule.recurse=1`. + - #2071 PR by @asottile. + - #2063 issue by @a666. +- ban broken importlib-resources versions. + - #2098 PR by @asottile. +- replace `exit(...)` with `raise SystemExit(...)` for portability. + - #2103 PR by @asottile. + - #2104 PR by @asottile. + + 2.15.0 - 2021-09-02 =================== diff --git a/setup.cfg b/setup.cfg index 975dd77b8..02669c702 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.15.0 +version = 2.16.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From a737d5fe2f083150b31425777eed0803a0ca23e0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 30 Nov 2021 18:19:36 -0500 Subject: [PATCH 576/967] add setuptools to the zipapp. resolves #2122 --- testing/zipapp/make | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/zipapp/make b/testing/zipapp/make index 55b6d2c7e..effc81234 100755 --- a/testing/zipapp/make +++ b/testing/zipapp/make @@ -71,7 +71,7 @@ def main() -> int: _msg('populating wheels...') _exit_if_retv( 'podman', 'run', '--rm', '--volume', f'{wheeldir}:/wheels:rw', IMG, - 'pip', 'wheel', f'pre_commit=={args.version}', + 'pip', 'wheel', f'pre_commit=={args.version}', 'setuptools', '--wheel-dir', '/wheels', ) From d4ffa5befb7725374be48fc7f78fcd438bd85361 Mon Sep 17 00:00:00 2001 From: Tony Rintala Date: Sat, 4 Dec 2021 22:22:21 +0200 Subject: [PATCH 577/967] fix: Add missing warning for regular expression with [\\/] test: Test case parameters for said regular expression refactor: For-loop for regex warnings instead of multiple if statements resolves #2151 --- pre_commit/clientlib.py | 38 ++++++++++++++------------------------ tests/clientlib_test.py | 10 ++++++++++ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 6377a8b67..a224cc931 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -144,18 +144,13 @@ def check(self, dct: Dict[str, Any]) -> None: f"regex, not a glob -- matching '/*' probably isn't what you " f'want here', ) - if r'[\/]' in dct.get(self.key, ''): - logger.warning( - fr'pre-commit normalizes slashes in the {self.key!r} field ' - fr'in hook {dct.get("id")!r} to forward slashes, so you ' - fr'can use / instead of [\/]', - ) - if r'[/\\]' in dct.get(self.key, ''): - logger.warning( - fr'pre-commit normalizes slashes in the {self.key!r} field ' - fr'in hook {dct.get("id")!r} to forward slashes, so you ' - fr'can use / instead of [/\\]', - ) + for fwd_slash_re in [r'[\\/]', r'[\/]', r'[/\\]']: + if fwd_slash_re in dct.get(self.key, ''): + logger.warning( + fr'pre-commit normalizes slashes in the {self.key!r} ' + fr'field in hook {dct.get("id")!r} to forward slashes, ' + fr'so you can use / instead of {fwd_slash_re}', + ) class OptionalSensibleRegexAtTop(cfgv.OptionalNoDefault): @@ -167,18 +162,13 @@ def check(self, dct: Dict[str, Any]) -> None: f'The top-level {self.key!r} field is a regex, not a glob -- ' f"matching '/*' probably isn't what you want here", ) - if r'[\/]' in dct.get(self.key, ''): - logger.warning( - fr'pre-commit normalizes the slashes in the top-level ' - fr'{self.key!r} field to forward slashes, so you can use / ' - fr'instead of [\/]', - ) - if r'[/\\]' in dct.get(self.key, ''): - logger.warning( - fr'pre-commit normalizes the slashes in the top-level ' - fr'{self.key!r} field to forward slashes, so you can use / ' - fr'instead of [/\\]', - ) + for fwd_slash_re in [r'[\\/]', r'[\/]', r'[/\\]']: + if fwd_slash_re in dct.get(self.key, ''): + logger.warning( + fr'pre-commit normalizes the slashes in the top-level ' + fr'{self.key!r} field to forward slashes, so you ' + fr'can use / instead of {fwd_slash_re}', + ) class MigrateShaToRev: diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 5427b1dae..a2be51b65 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -265,6 +265,11 @@ def test_warn_mutable_rev_conditional(): r"pre-commit normalizes slashes in the 'files' field in hook " r"'flake8' to forward slashes, so you can use / instead of [/\\]", ), + ( + r'dir[\\/].*\.py', + r"pre-commit normalizes slashes in the 'files' field in hook " + r"'flake8' to forward slashes, so you can use / instead of [\\/]", + ), ), ) def test_validate_optional_sensible_regex_at_hook(caplog, regex, warning): @@ -295,6 +300,11 @@ def test_validate_optional_sensible_regex_at_hook(caplog, regex, warning): r"pre-commit normalizes the slashes in the top-level 'files' " r'field to forward slashes, so you can use / instead of [/\\]', ), + ( + r'dir[\\/].*\.py', + r"pre-commit normalizes the slashes in the top-level 'files' " + r'field to forward slashes, so you can use / instead of [\\/]', + ), ), ) def test_validate_optional_sensible_regex_at_top_level(caplog, regex, warning): From b5088ceca6f51e46ca9406c8337602f2ca58c796 Mon Sep 17 00:00:00 2001 From: Tony Rintala Date: Sun, 5 Dec 2021 01:35:43 +0200 Subject: [PATCH 578/967] fix: regex lists to regex tuples --- pre_commit/clientlib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index a224cc931..b8f236893 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -144,7 +144,7 @@ def check(self, dct: Dict[str, Any]) -> None: f"regex, not a glob -- matching '/*' probably isn't what you " f'want here', ) - for fwd_slash_re in [r'[\\/]', r'[\/]', r'[/\\]']: + for fwd_slash_re in (r'[\\/]', r'[\/]', r'[/\\]'): if fwd_slash_re in dct.get(self.key, ''): logger.warning( fr'pre-commit normalizes slashes in the {self.key!r} ' @@ -162,7 +162,7 @@ def check(self, dct: Dict[str, Any]) -> None: f'The top-level {self.key!r} field is a regex, not a glob -- ' f"matching '/*' probably isn't what you want here", ) - for fwd_slash_re in [r'[\\/]', r'[\/]', r'[/\\]']: + for fwd_slash_re in (r'[\\/]', r'[\/]', r'[/\\]'): if fwd_slash_re in dct.get(self.key, ''): logger.warning( fr'pre-commit normalizes the slashes in the top-level ' From 379db4cb880b30b05863035d2d29a1f66b5795d9 Mon Sep 17 00:00:00 2001 From: Ralf Schmitt Date: Wed, 15 Dec 2021 02:21:23 +0100 Subject: [PATCH 579/967] Use 'go install' instead of 'go get' `go install` is the recommended way to install modules starting from go 1.16. In go 1.18 `go get` cannot be used anymore to install packages [1]. go 1.18 is not released yet. [1] https://tip.golang.org/doc/go1.18#go-command --- pre_commit/languages/golang.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index d6165d95e..10ebc6280 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -79,9 +79,11 @@ def install_environment( gopath = directory env = dict(os.environ, GOPATH=gopath) env.pop('GOBIN', None) - cmd_output_b('go', 'get', './...', cwd=repo_src_dir, env=env) + cmd_output_b('go', 'install', './...', cwd=repo_src_dir, env=env) for dependency in additional_dependencies: - cmd_output_b('go', 'get', dependency, cwd=repo_src_dir, env=env) + cmd_output_b( + 'go', 'install', dependency, cwd=repo_src_dir, env=env, + ) # Same some disk space, we don't need these after installation rmtree(prefix.path(directory, 'src')) pkgdir = prefix.path(directory, 'pkg') From a781bfb063e326d0b95df3c9c5277a9cb0b8aa08 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 Dec 2021 19:42:45 +0000 Subject: [PATCH 580/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.910-1 → v0.920](https://github.com/pre-commit/mirrors-mypy/compare/v0.910-1...v0.920) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 49517c341..4f024b41a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.910-1 + rev: v0.920 hooks: - id: mypy additional_dependencies: [types-all] From f637ac860312e89c122a2fb3d146a8c339f883a2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Dec 2021 17:01:51 -0500 Subject: [PATCH 581/967] minor py2 cleanup for sys.stderr.buffer --- tests/languages/helpers_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index 669cd3343..fd9b9a459 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -72,8 +72,8 @@ def test_basic_healthy(): def test_failed_setup_command_does_not_unicode_error(): script = ( 'import sys\n' - "getattr(sys.stderr, 'buffer', sys.stderr).write(b'\\x81\\xfe')\n" - 'exit(1)\n' + "sys.stderr.buffer.write(b'\\x81\\xfe')\n" + 'raise SystemExit(1)\n' ) # an assertion that this does not raise `UnicodeError` From 3512e441f4d88e2892593db6f3eb9bba8ced5eb3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Dec 2021 17:38:59 -0500 Subject: [PATCH 582/967] replace echo image with focal --- testing/resources/docker_hooks_repo/Dockerfile | 2 +- .../resources/docker_image_hooks_repo/.pre-commit-hooks.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/testing/resources/docker_hooks_repo/Dockerfile b/testing/resources/docker_hooks_repo/Dockerfile index 841b151ba..0bd1de0cf 100644 --- a/testing/resources/docker_hooks_repo/Dockerfile +++ b/testing/resources/docker_hooks_repo/Dockerfile @@ -1,3 +1,3 @@ -FROM cogniteev/echo +FROM ubuntu:focal CMD ["echo", "This is overwritten by the .pre-commit-hooks.yaml 'entry'"] diff --git a/testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml index 1b385aa12..e9fb24569 100644 --- a/testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml +++ b/testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml @@ -1,8 +1,8 @@ - id: echo-entrypoint name: echo (via --entrypoint) language: docker_image - entry: --entrypoint echo cogniteev/echo + entry: --entrypoint echo ubuntu:focal - id: echo-cmd name: echo (via cmd) language: docker_image - entry: cogniteev/echo echo + entry: ubuntu:focal echo From 42b0a263a6701955c6af350addb1a1e85f5e6342 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 22 Dec 2021 11:30:55 -0800 Subject: [PATCH 583/967] run dead, remove dead code via https://github.com/asottile/dead --- pre_commit/commands/install_uninstall.py | 25 -------------- pre_commit/commands/run.py | 3 +- tests/commands/install_uninstall_test.py | 43 +----------------------- 3 files changed, 2 insertions(+), 69 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 7974423b6..fad6c6421 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -1,4 +1,3 @@ -import itertools import logging import os.path import shlex @@ -31,10 +30,6 @@ CURRENT_HASH = b'138fd403232d2ddd5efb44317e38bf03' TEMPLATE_START = '# start templated\n' TEMPLATE_END = '# end templated\n' -# Homebrew/homebrew-core#35825: be more timid about appropriate `PATH` -# #1312 os.defpath is too restrictive on BSD -POSIX_SEARCH_PATH = ('/usr/local/bin', '/usr/bin', '/bin') -SYS_EXE = os.path.basename(os.path.realpath(sys.executable)) def _hook_paths( @@ -54,26 +49,6 @@ def is_our_script(filename: str) -> bool: return any(h in contents for h in (CURRENT_HASH,) + PRIOR_HASHES) -def shebang() -> str: - if sys.platform == 'win32': - py, _ = os.path.splitext(SYS_EXE) - else: - exe_choices = [ - f'python{sys.version_info[0]}.{sys.version_info[1]}', - f'python{sys.version_info[0]}', - ] - # avoid searching for bare `python` as it's likely to be python 2 - if SYS_EXE != 'python': - exe_choices.append(SYS_EXE) - for path, exe in itertools.product(POSIX_SEARCH_PATH, exe_choices): - if os.access(os.path.join(path, exe), os.X_OK): - py = exe - break - else: - py = SYS_EXE - return f'#!/usr/bin/env {py}' - - def _install_hook_script( config_file: str, hook_type: str, diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 2714faf40..f8ced0f9b 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -275,7 +275,6 @@ def _run_hooks( hooks: Sequence[Hook], skips: Set[str], args: argparse.Namespace, - environ: MutableMapping[str, str], ) -> int: """Actually run the hooks.""" cols = _compute_cols(hooks) @@ -416,7 +415,7 @@ def run( to_install = [hook for hook in hooks if hook.id not in skips] install_hook_envs(to_install, store) - return _run_hooks(config, hooks, skips, args, environ) + return _run_hooks(config, hooks, skips, args) # https://github.com/python/mypy/issues/7726 raise AssertionError('unreachable') diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 833990349..0b2e248b1 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -1,19 +1,15 @@ import os.path import re -import sys -from unittest import mock import re_assert import pre_commit.constants as C from pre_commit import git -from pre_commit.commands import install_uninstall from pre_commit.commands.install_uninstall import CURRENT_HASH from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import install_hooks from pre_commit.commands.install_uninstall import is_our_script from pre_commit.commands.install_uninstall import PRIOR_HASHES -from pre_commit.commands.install_uninstall import shebang from pre_commit.commands.install_uninstall import uninstall from pre_commit.parse_shebang import find_executable from pre_commit.util import cmd_output @@ -43,43 +39,6 @@ def test_is_previous_pre_commit(tmpdir): assert is_our_script(f.strpath) -def patch_platform(platform): - return mock.patch.object(sys, 'platform', platform) - - -def patch_lookup_path(path): - return mock.patch.object(install_uninstall, 'POSIX_SEARCH_PATH', path) - - -def patch_sys_exe(exe): - return mock.patch.object(install_uninstall, 'SYS_EXE', exe) - - -def test_shebang_windows(): - with patch_platform('win32'), patch_sys_exe('python'): - assert shebang() == '#!/usr/bin/env python' - - -def test_shebang_windows_drop_ext(): - with patch_platform('win32'), patch_sys_exe('python.exe'): - assert shebang() == '#!/usr/bin/env python' - - -def test_shebang_posix_not_on_path(): - with patch_platform('posix'), patch_lookup_path(()): - with patch_sys_exe('python3.6'): - assert shebang() == '#!/usr/bin/env python3.6' - - -def test_shebang_posix_on_path(tmpdir): - exe = tmpdir.join(f'python{sys.version_info[0]}').ensure() - make_executable(exe) - - with patch_platform('posix'), patch_lookup_path((tmpdir.strpath,)): - with patch_sys_exe('python'): - assert shebang() == f'#!/usr/bin/env python{sys.version_info[0]}' - - def test_install_pre_commit(in_git_dir, store): assert not install(C.CONFIG_FILE, store, hook_types=['pre-commit']) assert os.access(in_git_dir.join('.git/hooks/pre-commit').strpath, os.X_OK) @@ -336,7 +295,7 @@ 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(f'{shebang()}\nprint("legacy hook")\n') + f.write('#!/usr/bin/env bash\necho legacy hook\n') make_executable(f.name) From ba496b836911be1a5f139182fc118ef8b2f873a1 Mon Sep 17 00:00:00 2001 From: Lorenz Walthert Date: Sat, 11 Dec 2021 18:35:57 +0100 Subject: [PATCH 584/967] better r path detection --- pre_commit/languages/r.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index d573775f7..74ecc6a85 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -54,6 +54,12 @@ def _prefix_if_non_local_file_entry( path = prefix.path(entry[1]) return (path,) +def _rscript_exec(): + """ + When invoked in a sub-process of R, use full path + """ + return os.path.join(os.getenv('R_HOME', ""), 'Rscript') + def _entry_validate(entry: Sequence[str]) -> None: """ @@ -95,8 +101,9 @@ def install_environment( os.makedirs(env_dir, exist_ok=True) shutil.copy(prefix.path('renv.lock'), env_dir) shutil.copytree(prefix.path('renv'), os.path.join(env_dir, 'renv')) + cmd_output_b( - 'Rscript', '--vanilla', '-e', + _rscript_exec(), '--vanilla', '-e', f"""\ prefix_dir <- {prefix.prefix_dir!r} options( @@ -130,7 +137,7 @@ def install_environment( if additional_dependencies: with in_env(prefix, version): cmd_output_b( - 'Rscript', *RSCRIPT_OPTS, '-e', + _rscript_exec(), *RSCRIPT_OPTS, '-e', 'renv::install(commandArgs(trailingOnly = TRUE))', *additional_dependencies, cwd=env_dir, From b7331b653abca2f7dc711b452b60c29889888d89 Mon Sep 17 00:00:00 2001 From: Lorenz Walthert Date: Fri, 24 Dec 2021 14:36:43 +0100 Subject: [PATCH 585/967] unset renv project --- pre_commit/languages/r.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 74ecc6a85..98a8ec4f6 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -8,6 +8,7 @@ from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import UNSET from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix @@ -23,6 +24,7 @@ def get_env_patch(venv: str) -> PatchesT: return ( ('R_PROFILE_USER', os.path.join(venv, 'activate.R')), + ('RENV_PROJECT', UNSET), ) @@ -54,11 +56,12 @@ def _prefix_if_non_local_file_entry( path = prefix.path(entry[1]) return (path,) -def _rscript_exec(): + +def _rscript_exec() -> str: """ When invoked in a sub-process of R, use full path """ - return os.path.join(os.getenv('R_HOME', ""), 'Rscript') + return os.path.join(os.getenv('R_HOME', ''), 'Rscript') def _entry_validate(entry: Sequence[str]) -> None: @@ -101,7 +104,7 @@ def install_environment( os.makedirs(env_dir, exist_ok=True) shutil.copy(prefix.path('renv.lock'), env_dir) shutil.copytree(prefix.path('renv'), os.path.join(env_dir, 'renv')) - + cmd_output_b( _rscript_exec(), '--vanilla', '-e', f"""\ From 1617692f12b8b67a471e6e810be49871b73ea056 Mon Sep 17 00:00:00 2001 From: Lorenz Walthert Date: Fri, 24 Dec 2021 14:52:46 +0100 Subject: [PATCH 586/967] no docs --- pre_commit/languages/r.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 98a8ec4f6..e034e3904 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -58,9 +58,6 @@ def _prefix_if_non_local_file_entry( def _rscript_exec() -> str: - """ - When invoked in a sub-process of R, use full path - """ return os.path.join(os.getenv('R_HOME', ''), 'Rscript') From d7ac975454d66ab1003d8522dbeb2054c3d86f31 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 Dec 2021 20:06:12 +0000 Subject: [PATCH 587/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.0.1 → v4.1.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.0.1...v4.1.0) - [github.com/pre-commit/mirrors-mypy: v0.920 → v0.930](https://github.com/pre-commit/mirrors-mypy/compare/v0.920...v0.930) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4f024b41a..e9b75d21f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.1.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -44,7 +44,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.920 + rev: v0.930 hooks: - id: mypy additional_dependencies: [types-all] From 83675fe7687def4b5a673fd794c9472c31fe69e4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 27 Dec 2021 18:32:56 -0500 Subject: [PATCH 588/967] work around python/mypy#11852 --- pre_commit/xargs.py | 3 ++- tests/xargs_test.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 6b0fa2086..9a397234a 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -159,7 +159,8 @@ def run_cmd_partition( ) threads = min(len(partitions), target_concurrency) - with _thread_mapper(threads) as thread_map: + # https://github.com/python/mypy/issues/11852 + with _thread_mapper(threads) as thread_map: # type: ignore results = thread_map(run_cmd_partition, partitions) for proc_retcode, proc_out, _ in results: diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 7e83ef590..80bcd2680 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -166,13 +166,15 @@ def test_xargs_concurrency(): def test_thread_mapper_concurrency_uses_threadpoolexecutor_map(): - with xargs._thread_mapper(10) as thread_map: + # https://github.com/python/mypy/issues/11852 + with xargs._thread_mapper(10) as thread_map: # type: ignore _self = thread_map.__self__ # type: ignore assert isinstance(_self, concurrent.futures.ThreadPoolExecutor) def test_thread_mapper_concurrency_uses_regular_map(): - with xargs._thread_mapper(1) as thread_map: + # https://github.com/python/mypy/issues/11852 + with xargs._thread_mapper(1) as thread_map: # type: ignore assert thread_map is map From d3b4f737b92eeae041d1125a42897075a1816f35 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 31 Dec 2021 17:31:12 -0800 Subject: [PATCH 589/967] forbid overriding `entry` for meta hooks --- pre_commit/clientlib.py | 9 +++++++++ tests/clientlib_test.py | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index b8f236893..47ebd54f7 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -251,12 +251,21 @@ def warn_unknown_keys_repo( ), ) + +class NotAllowed(cfgv.OptionalNoDefault): + def check(self, dct: Dict[str, Any]) -> None: + if self.key in dct: + raise cfgv.ValidationError(f'{self.key!r} cannot be overridden') + + META_HOOK_DICT = cfgv.Map( 'Hook', 'id', cfgv.Required('id', cfgv.check_string), cfgv.Required('id', cfgv.check_one_of(tuple(k for k, _ in _meta))), # language must be system cfgv.Optional('language', cfgv.check_one_of({'system'}), 'system'), + # entry cannot be overridden + NotAllowed('entry', cfgv.check_any), *( # default to the hook definition for the meta hooks cfgv.ConditionalOptional(key, cfgv.check_any, value, 'id', hook_id) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index a2be51b65..39a371689 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -423,6 +423,13 @@ def test_migrate_to_sha_ok(): {'repo': 'meta', 'hooks': [{'id': 'identity', 'language': 'python'}]}, # name override must be string {'repo': 'meta', 'hooks': [{'id': 'identity', 'name': False}]}, + pytest.param( + { + 'repo': 'meta', + 'hooks': [{'id': 'identity', 'entry': 'echo hi'}], + }, + id='cannot override entry for meta hooks', + ), ), ) def test_meta_hook_invalid(config_repo): From 8be0a10e91a999c3271f1d7afd1eb930a06fffb6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Jan 2022 20:02:48 +0000 Subject: [PATCH 590/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-autopep8: v1.5.7 → v1.6.0](https://github.com/pre-commit/mirrors-autopep8/compare/v1.5.7...v1.6.0) - [github.com/asottile/pyupgrade: v2.29.1 → v2.31.0](https://github.com/asottile/pyupgrade/compare/v2.29.1...v2.31.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e9b75d21f..48d9b106a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: flake8 additional_dependencies: [flake8-typing-imports==1.10.0] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.5.7 + rev: v1.6.0 hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.29.1 + rev: v2.31.0 hooks: - id: pyupgrade args: [--py36-plus] From e3dc3f7934b206a1de16dc6800f9ae02ee73dd11 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 5 Jan 2022 08:14:43 -0800 Subject: [PATCH 591/967] always use #!/bin/sh on windows --- pre_commit/commands/install_uninstall.py | 7 +++++++ requirements-dev.txt | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index fad6c6421..50c644320 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -82,6 +82,13 @@ def _install_hook_script( before, rest = contents.split(TEMPLATE_START) _, after = rest.split(TEMPLATE_END) + # on windows always use `/bin/sh` since `bash` might not be on PATH + # though we use bash-specific features `sh` on windows is actually + # bash in "POSIXLY_CORRECT" mode which still supports the features we + # use: subshells / arrays + if sys.platform == 'win32': # pragma: win32 cover + hook_file.write('#!/bin/sh\n') + hook_file.write(before + TEMPLATE_START) hook_file.write(f'INSTALL_PYTHON={shlex.quote(sys.executable)}\n') # TODO: python3.8+: shlex.join diff --git a/requirements-dev.txt b/requirements-dev.txt index 3a7b11cbf..a23a37300 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ -covdefaults>=2.1 +covdefaults>=2.2 coverage distlib pytest From a33773182e9dab5d963274eb75ee2d5fd7313398 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Jan 2022 20:21:20 +0000 Subject: [PATCH 592/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.930 → v0.931](https://github.com/pre-commit/mirrors-mypy/compare/v0.930...v0.931) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 48d9b106a..49eab3fa3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.930 + rev: v0.931 hooks: - id: mypy additional_dependencies: [types-all] From bba6cf4296c7cb9c9ce0234aadf901de5210841f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 10 Jan 2022 15:35:33 -0500 Subject: [PATCH 593/967] Revert "work around python/mypy#11852" This reverts commit 83675fe7687def4b5a673fd794c9472c31fe69e4. --- pre_commit/xargs.py | 3 +-- tests/xargs_test.py | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 9a397234a..6b0fa2086 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -159,8 +159,7 @@ def run_cmd_partition( ) threads = min(len(partitions), target_concurrency) - # https://github.com/python/mypy/issues/11852 - with _thread_mapper(threads) as thread_map: # type: ignore + with _thread_mapper(threads) as thread_map: results = thread_map(run_cmd_partition, partitions) for proc_retcode, proc_out, _ in results: diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 80bcd2680..7e83ef590 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -166,15 +166,13 @@ def test_xargs_concurrency(): def test_thread_mapper_concurrency_uses_threadpoolexecutor_map(): - # https://github.com/python/mypy/issues/11852 - with xargs._thread_mapper(10) as thread_map: # type: ignore + with xargs._thread_mapper(10) as thread_map: _self = thread_map.__self__ # type: ignore assert isinstance(_self, concurrent.futures.ThreadPoolExecutor) def test_thread_mapper_concurrency_uses_regular_map(): - # https://github.com/python/mypy/issues/11852 - with xargs._thread_mapper(1) as thread_map: # type: ignore + with xargs._thread_mapper(1) as thread_map: assert thread_map is map From 428dc6e46eb68065bfc115419927949cdd056811 Mon Sep 17 00:00:00 2001 From: Jamie Alessio Date: Thu, 13 Jan 2022 12:14:58 -0800 Subject: [PATCH 594/967] Update rbenv / ruby-build versions --- pre_commit/resources/rbenv.tar.gz | Bin 32678 -> 32551 bytes pre_commit/resources/ruby-build.tar.gz | Bin 68689 -> 71151 bytes testing/make-archives | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/resources/rbenv.tar.gz b/pre_commit/resources/rbenv.tar.gz index 98b7e0f60d5437171a82991b983ac52455a328a7..da2514e71145e91debbad4bb1e11ca6d95ed3f63 100644 GIT binary patch literal 32551 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

^Q`cwPbh>`Bvvp*+bPDup2@ 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 H7|8zs@v_%p literal 32678 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 bIBt_ZJ@3s)LTG2z`Ty+9&mC15GPoH4$&&|1 diff --git a/testing/make-archives b/testing/make-archives index 707fd8843..ce098ba1f 100755 --- a/testing/make-archives +++ b/testing/make-archives @@ -15,8 +15,8 @@ from typing import Sequence REPOS = ( - ('rbenv', 'https://github.com/rbenv/rbenv', '585ed84'), - ('ruby-build', 'https://github.com/rbenv/ruby-build', 'e9fa4bf'), + ('rbenv', 'https://github.com/rbenv/rbenv', '38e1fbb'), + ('ruby-build', 'https://github.com/rbenv/ruby-build', '8663d2f'), ( 'ruby-download', 'https://github.com/garnieretienne/rvm-download', From 83aa65c4291b8a1a134cd024fbe071323f400c83 Mon Sep 17 00:00:00 2001 From: "Uwe L. Korn" Date: Sat, 15 Jan 2022 09:33:40 +0100 Subject: [PATCH 595/967] Add mamba support to `language: conda` --- pre_commit/languages/conda.py | 15 ++++++++++++-- tests/languages/conda_test.py | 38 +++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 tests/languages/conda_test.py diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index d634e4931..97e2f69eb 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -50,6 +50,15 @@ def in_env( yield +def _conda_exe() -> str: + if os.environ.get('PRE_COMMIT_USE_MICROMAMBA'): + return 'micromamba' + elif os.environ.get('PRE_COMMIT_USE_MAMBA'): + return 'mamba' + else: + return 'conda' + + def install_environment( prefix: Prefix, version: str, @@ -58,15 +67,17 @@ def install_environment( helpers.assert_version_default('conda', version) directory = helpers.environment_dir(ENVIRONMENT_DIR, version) + conda_exe = _conda_exe() + env_dir = prefix.path(directory) with clean_path_on_failure(env_dir): cmd_output_b( - 'conda', 'env', 'create', '-p', env_dir, '--file', + conda_exe, 'env', 'create', '-p', env_dir, '--file', 'environment.yml', cwd=prefix.prefix_dir, ) if additional_dependencies: cmd_output_b( - 'conda', 'install', '-p', env_dir, *additional_dependencies, + conda_exe, 'install', '-p', env_dir, *additional_dependencies, cwd=prefix.prefix_dir, ) diff --git a/tests/languages/conda_test.py b/tests/languages/conda_test.py new file mode 100644 index 000000000..6faa78f21 --- /dev/null +++ b/tests/languages/conda_test.py @@ -0,0 +1,38 @@ +import pytest + +from pre_commit import envcontext +from pre_commit.languages.conda import _conda_exe + + +@pytest.mark.parametrize( + ('ctx', 'expected'), + ( + pytest.param( + ( + ('PRE_COMMIT_USE_MICROMAMBA', envcontext.UNSET), + ('PRE_COMMIT_USE_MAMBA', envcontext.UNSET), + ), + 'conda', + id='default', + ), + pytest.param( + ( + ('PRE_COMMIT_USE_MICROMAMBA', '1'), + ('PRE_COMMIT_USE_MAMBA', ''), + ), + 'micromamba', + id='default', + ), + pytest.param( + ( + ('PRE_COMMIT_USE_MICROMAMBA', ''), + ('PRE_COMMIT_USE_MAMBA', '1'), + ), + 'mamba', + id='default', + ), + ), +) +def test_conda_exe(ctx, expected): + with envcontext.envcontext(ctx): + assert _conda_exe() == expected From c05f58b776603dc2a5222f035c2dc058426497de Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 16 Jan 2022 07:20:12 -0800 Subject: [PATCH 596/967] add git version to error output --- pre_commit/error_handler.py | 5 +++++ tests/error_handler_test.py | 1 + 2 files changed, 6 insertions(+) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 023dd3596..7e74b9589 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -9,6 +9,7 @@ from pre_commit import output from pre_commit.errors import FatalError from pre_commit.store import Store +from pre_commit.util import cmd_output_b from pre_commit.util import force_bytes @@ -21,6 +22,9 @@ def _log_and_exit( error_msg = f'{msg}: {type(exc).__name__}: '.encode() + force_bytes(exc) output.write_line_b(error_msg) + _, git_version_b, _ = cmd_output_b('git', '--version', retcode=None) + git_version = git_version_b.decode(errors='backslashreplace').rstrip() + storedir = Store().directory log_path = os.path.join(storedir, 'pre-commit.log') with contextlib.ExitStack() as ctx: @@ -38,6 +42,7 @@ def _log_and_exit( _log_line() _log_line('```') _log_line(f'pre-commit version: {C.VERSION}') + _log_line(f'git --version: {git_version}') _log_line('sys.version:') for line in sys.version.splitlines(): _log_line(f' {line}') diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 6b0bb86d7..cb76dcf47 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -122,6 +122,7 @@ def test_log_and_exit(cap_out, mock_store_dir): r'\n' r'```\n' r'pre-commit version: \d+\.\d+\.\d+\n' + r'git --version: git version .+\n' r'sys.version:\n' r'( .*\n)*' r'sys.executable: .*\n' From 3f8be7400d523fafe8c6d2d0fa4fb1560e7ae21d Mon Sep 17 00:00:00 2001 From: Matt Layman Date: Sun, 12 Dec 2021 01:57:54 -0500 Subject: [PATCH 597/967] Add naive and untested version of Lua language support. --- azure-pipelines.yml | 4 + pre_commit/languages/all.py | 2 + pre_commit/languages/lua.py | 150 ++++++++++++++++++ ...template_pre-commit-package-dev-1.rockspec | 12 ++ pre_commit/store.py | 3 +- testing/gen-languages-all | 2 +- testing/get-lua.sh | 5 + .../resources/lua_repo/.pre-commit-hooks.yaml | 4 + .../resources/lua_repo/bin/hello-world-lua | 3 + .../resources/lua_repo/hello-dev-1.rockspec | 15 ++ testing/util.py | 4 + tests/languages/lua_test.py | 55 +++++++ tests/repository_test.py | 29 ++++ 13 files changed, 286 insertions(+), 2 deletions(-) create mode 100644 pre_commit/languages/lua.py create mode 100644 pre_commit/resources/empty_template_pre-commit-package-dev-1.rockspec create mode 100755 testing/get-lua.sh create mode 100644 testing/resources/lua_repo/.pre-commit-hooks.yaml create mode 100755 testing/resources/lua_repo/bin/hello-world-lua create mode 100644 testing/resources/lua_repo/hello-dev-1.rockspec create mode 100644 tests/languages/lua_test.py diff --git a/azure-pipelines.yml b/azure-pipelines.yml index a468e8aa6..d8cbd11d5 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -42,6 +42,8 @@ jobs: displayName: install coursier - bash: testing/get-dart.sh displayName: install dart + - bash: testing/get-lua.sh + displayName: install lua - bash: testing/get-swift.sh displayName: install swift - bash: testing/get-r.sh @@ -56,6 +58,8 @@ jobs: displayName: install coursier - bash: testing/get-dart.sh displayName: install dart + - bash: testing/get-lua.sh + displayName: install lua - bash: testing/get-swift.sh displayName: install swift - bash: testing/get-r.sh diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index d8a364c5d..0bcedd66d 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -13,6 +13,7 @@ from pre_commit.languages import dotnet from pre_commit.languages import fail from pre_commit.languages import golang +from pre_commit.languages import lua from pre_commit.languages import node from pre_commit.languages import perl from pre_commit.languages import pygrep @@ -51,6 +52,7 @@ class Language(NamedTuple): 'dotnet': Language(name='dotnet', ENVIRONMENT_DIR=dotnet.ENVIRONMENT_DIR, get_default_version=dotnet.get_default_version, healthy=dotnet.healthy, install_environment=dotnet.install_environment, run_hook=dotnet.run_hook), # noqa: E501 'fail': Language(name='fail', ENVIRONMENT_DIR=fail.ENVIRONMENT_DIR, get_default_version=fail.get_default_version, healthy=fail.healthy, install_environment=fail.install_environment, run_hook=fail.run_hook), # noqa: E501 'golang': Language(name='golang', ENVIRONMENT_DIR=golang.ENVIRONMENT_DIR, get_default_version=golang.get_default_version, healthy=golang.healthy, install_environment=golang.install_environment, run_hook=golang.run_hook), # noqa: E501 + 'lua': Language(name='lua', ENVIRONMENT_DIR=lua.ENVIRONMENT_DIR, get_default_version=lua.get_default_version, healthy=lua.healthy, install_environment=lua.install_environment, run_hook=lua.run_hook), # noqa: E501 'node': Language(name='node', ENVIRONMENT_DIR=node.ENVIRONMENT_DIR, get_default_version=node.get_default_version, healthy=node.healthy, install_environment=node.install_environment, run_hook=node.run_hook), # noqa: E501 'perl': Language(name='perl', ENVIRONMENT_DIR=perl.ENVIRONMENT_DIR, get_default_version=perl.get_default_version, healthy=perl.healthy, install_environment=perl.install_environment, run_hook=perl.run_hook), # noqa: E501 'pygrep': Language(name='pygrep', ENVIRONMENT_DIR=pygrep.ENVIRONMENT_DIR, get_default_version=pygrep.get_default_version, healthy=pygrep.healthy, install_environment=pygrep.install_environment, run_hook=pygrep.run_hook), # noqa: E501 diff --git a/pre_commit/languages/lua.py b/pre_commit/languages/lua.py new file mode 100644 index 000000000..ae322279b --- /dev/null +++ b/pre_commit/languages/lua.py @@ -0,0 +1,150 @@ +import contextlib +import os +import re +from typing import Generator +from typing import Sequence +from typing import Tuple + +import pre_commit.constants as C +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import Var +from pre_commit.hook import Hook +from pre_commit.languages import helpers +from pre_commit.parse_shebang import find_executable +from pre_commit.prefix import Prefix +from pre_commit.util import clean_path_on_failure +from pre_commit.util import cmd_output + +ENVIRONMENT_DIR = 'lua_env' +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy + + +def _find_lua(language_version: str) -> str: # pragma: win32 no cover + """Find a lua executable. + + Lua doesn't always have a plain `lua` executable. + Some OS vendors will ship the binary as `lua#.#` (e.g., lua5.3) + so discovery is needed to find a valid executable. + """ + if language_version == C.DEFAULT: + choices = ['lua'] + for path in os.environ.get('PATH', '').split(os.pathsep): + try: + candidates = os.listdir(path) + except OSError: + # Invalid path on PATH or lacking permissions. + continue + + for candidate in candidates: + # The Lua executable might look like `lua#.#` or `lua-#.#`. + if re.search(r'^lua[-]?\d+\.\d+', candidate): + choices.append(candidate) + else: + # Prefer version specific executables first if available. + # This should avoid the corner case where a user requests a language + # version, gets a `lua` executable, but that executable is actually + # for a different version and package.path would patch LUA_PATH + # incorrectly. + choices = [f'lua{language_version}', 'lua-{language_version}', 'lua'] + + found_exes = [exe for exe in choices if find_executable(exe)] + if found_exes: + return found_exes[0] + + raise ValueError( + 'No lua executable found on the system paths ' + f'for {language_version} version.', + ) + + +def _get_lua_path_version( + lua_executable: str, +) -> str: # pragma: win32 no cover + """Get the Lua version used in file paths.""" + # This could sniff out from _VERSION, but checking package.path should + # provide an answer for *exactly* where lua is looking for packages. + _, stdout, _ = cmd_output(lua_executable, '-e', 'print(package.path)') + sep = os.sep if os.name != 'nt' else os.sep * 2 + match = re.search(fr'{sep}lua{sep}(.*?){sep}', stdout) + if match: + return match[1] + + raise ValueError('Cannot determine lua version for file paths.') + + +def get_env_patch( + env: str, language_version: str, +) -> PatchesT: # pragma: win32 no cover + lua = _find_lua(language_version) + version = _get_lua_path_version(lua) + return ( + ('PATH', (os.path.join(env, 'bin'), os.pathsep, Var('PATH'))), + ( + 'LUA_PATH', ( + os.path.join(env, 'share', 'lua', version, '?.lua;'), + os.path.join(env, 'share', 'lua', version, '?', 'init.lua;;'), + ), + ), + ( + 'LUA_CPATH', ( + os.path.join(env, 'lib', 'lua', version, '?.so;;'), + ), + ), + ) + + +def _envdir(prefix: Prefix, version: str) -> str: # pragma: win32 no cover + directory = helpers.environment_dir(ENVIRONMENT_DIR, version) + return prefix.path(directory) + + +@contextlib.contextmanager # pragma: win32 no cover +def in_env( + prefix: Prefix, + language_version: str, +) -> Generator[None, None, None]: + with envcontext( + get_env_patch( + _envdir(prefix, language_version), language_version, + ), + ): + yield + + +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: # pragma: win32 no cover + helpers.assert_version_default('lua', version) + + envdir = _envdir(prefix, version) + with clean_path_on_failure(envdir): + with in_env(prefix, version): + # luarocks doesn't bootstrap a tree prior to installing + # so ensure the directory exists. + os.makedirs(envdir, exist_ok=True) + + make_cmd = ['luarocks', '--tree', envdir, 'make'] + # Older luarocks (e.g., 2.4.2) expect the rockspec as an argument. + filenames = prefix.star('.rockspec') + make_cmd.extend(filenames[:1]) + + helpers.run_setup_cmd(prefix, tuple(make_cmd)) + + # luarocks can't install multiple packages at once + # so install them individually. + for dependency in additional_dependencies: + cmd = ('luarocks', '--tree', envdir, 'install', dependency) + helpers.run_setup_cmd(prefix, cmd) + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: # pragma: win32 no cover + with in_env(hook.prefix, hook.language_version): + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/resources/empty_template_pre-commit-package-dev-1.rockspec b/pre_commit/resources/empty_template_pre-commit-package-dev-1.rockspec new file mode 100644 index 000000000..f063c8e23 --- /dev/null +++ b/pre_commit/resources/empty_template_pre-commit-package-dev-1.rockspec @@ -0,0 +1,12 @@ +package = "pre-commit-package" +version = "dev-1" + +source = { + url = "git+ssh://git@github.com/pre-commit/pre-commit.git" +} +description = {} +dependencies = {} +build = { + type = "builtin", + modules = {}, +} diff --git a/pre_commit/store.py b/pre_commit/store.py index fc3bc511a..27d8553c8 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -188,7 +188,8 @@ def _git_cmd(*args: str) -> None: LOCAL_RESOURCES = ( 'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore', - 'package.json', 'pre_commit_placeholder_package.gemspec', 'setup.py', + 'package.json', 'pre-commit-package-dev-1.rockspec', + 'pre_commit_placeholder_package.gemspec', 'setup.py', 'environment.yml', 'Makefile.PL', 'pubspec.yaml', 'renv.lock', 'renv/activate.R', 'renv/LICENSE.renv', ) diff --git a/testing/gen-languages-all b/testing/gen-languages-all index c933c1435..152cf3c6f 100755 --- a/testing/gen-languages-all +++ b/testing/gen-languages-all @@ -3,7 +3,7 @@ import sys LANGUAGES = [ 'conda', 'coursier', 'dart', 'docker', 'docker_image', 'dotnet', 'fail', - 'golang', 'node', 'perl', 'pygrep', 'python', 'r', 'ruby', 'rust', + 'golang', 'lua', 'node', 'perl', 'pygrep', 'python', 'r', 'ruby', 'rust', 'script', 'swift', 'system', ] FIELDS = [ diff --git a/testing/get-lua.sh b/testing/get-lua.sh new file mode 100755 index 000000000..580e24772 --- /dev/null +++ b/testing/get-lua.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Install the runtime and package manager. +sudo apt install lua5.3 liblua5.3-dev luarocks diff --git a/testing/resources/lua_repo/.pre-commit-hooks.yaml b/testing/resources/lua_repo/.pre-commit-hooks.yaml new file mode 100644 index 000000000..767ef972c --- /dev/null +++ b/testing/resources/lua_repo/.pre-commit-hooks.yaml @@ -0,0 +1,4 @@ +- id: hello-world-lua + name: hello world lua + entry: hello-world-lua + language: lua diff --git a/testing/resources/lua_repo/bin/hello-world-lua b/testing/resources/lua_repo/bin/hello-world-lua new file mode 100755 index 000000000..2a0e00246 --- /dev/null +++ b/testing/resources/lua_repo/bin/hello-world-lua @@ -0,0 +1,3 @@ +#!/usr/bin/env lua + +print('hello world') diff --git a/testing/resources/lua_repo/hello-dev-1.rockspec b/testing/resources/lua_repo/hello-dev-1.rockspec new file mode 100644 index 000000000..82486e08a --- /dev/null +++ b/testing/resources/lua_repo/hello-dev-1.rockspec @@ -0,0 +1,15 @@ +package = "hello" +version = "dev-1" + +source = { + url = "git+ssh://git@github.com/pre-commit/pre-commit.git" +} +description = {} +dependencies = {} +build = { + type = "builtin", + modules = {}, + install = { + bin = {"bin/hello-world-lua"} + }, +} diff --git a/testing/util.py b/testing/util.py index 791a2b955..283ed4776 100644 --- a/testing/util.py +++ b/testing/util.py @@ -48,6 +48,10 @@ def cmd_output_mocked_pre_commit_home( os.name == 'nt' or not docker_is_running(), reason="Docker isn't running or can't be accessed", ) +skipif_cant_run_lua = pytest.mark.skipif( + os.name == 'nt', + reason="lua isn't installed or can't be found", +) skipif_cant_run_swift = pytest.mark.skipif( parse_shebang.find_executable('swift') is None, reason="swift isn't installed or can't be found", diff --git a/tests/languages/lua_test.py b/tests/languages/lua_test.py new file mode 100644 index 000000000..fba23b22f --- /dev/null +++ b/tests/languages/lua_test.py @@ -0,0 +1,55 @@ +import os +from unittest import mock + +import pytest + +import pre_commit.constants as C +from pre_commit.languages import lua +from testing.util import xfailif_windows + + +@pytest.mark.parametrize( + 'lua_name', ('lua', 'lua5.4', 'lua-5.4', 'lua5.4.exe'), +) +def test_find_lua(tmp_path, lua_name): + """The language support can find common lua executable names.""" + lua_file = tmp_path / lua_name + lua_file.touch(0o555) + with mock.patch.dict(os.environ, {'PATH': str(tmp_path)}): + lua_executable = lua._find_lua(C.DEFAULT) + assert lua_name in lua_executable + + +def test_find_lua_language_version(tmp_path): + """Language discovery can find a specific version.""" + lua_file = tmp_path / 'lua5.99' + lua_file.touch(0o555) + with mock.patch.dict(os.environ, {'PATH': str(tmp_path)}): + lua_executable = lua._find_lua('5.99') + assert 'lua5.99' in lua_executable + + +@pytest.mark.parametrize( + ('invalid', 'mode'), + ( + ('foobar', 0o555), + ('luac', 0o555), + # Windows doesn't respect the executable checking. + pytest.param('lua5.4', 0o444, marks=xfailif_windows), + ), +) +def test_find_lua_fail(tmp_path, invalid, mode): + """No lua executable on the system will fail.""" + non_lua_file = tmp_path / invalid + non_lua_file.touch(mode) + with mock.patch.dict(os.environ, {'PATH': str(tmp_path)}): + with pytest.raises(ValueError): + lua._find_lua(C.DEFAULT) + + +@mock.patch.object(lua, 'cmd_output') +def test_bad_package_path(mock_cmd_output): + """A package path missing path info returns an unknown version.""" + mock_cmd_output.return_value = (0, '', '') + with pytest.raises(ValueError): + lua._get_lua_path_version('lua') diff --git a/tests/repository_test.py b/tests/repository_test.py index 36268e1e8..5f5e17e55 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -17,6 +17,7 @@ from pre_commit.hook import Hook from pre_commit.languages import golang from pre_commit.languages import helpers +from pre_commit.languages import lua from pre_commit.languages import node from pre_commit.languages import python from pre_commit.languages import ruby @@ -34,6 +35,7 @@ from testing.util import get_resource_path from testing.util import skipif_cant_run_coursier from testing.util import skipif_cant_run_docker +from testing.util import skipif_cant_run_lua from testing.util import skipif_cant_run_swift from testing.util import xfailif_windows @@ -1128,3 +1130,30 @@ def test_non_installable_hook_error_for_additional_dependencies(store, caplog): 'using language `system` which does not install an environment. ' 'Perhaps you meant to use a specific language?' ) + + +@skipif_cant_run_lua # pragma: win32 no cover +def test_lua_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'lua_repo', + 'hello-world-lua', [], b'hello world\n', + ) + + +@skipif_cant_run_lua # pragma: win32 no cover +def test_local_lua_additional_dependencies(store): + lua_entry = lua._find_lua(C.DEFAULT) + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'local-lua', + 'name': 'local-lua', + 'entry': lua_entry, + 'language': 'lua', + 'args': ['-e', 'require "inspect"; print("hello world")'], + 'additional_dependencies': ['inspect'], + }], + } + hook = _get_hook(config, store, 'local-lua') + ret, out = _hook_run(hook, (), color=False) + assert (ret, _norm_out(out)) == (0, b'hello world\n') From 54331dca6fcfff1a06c43defb29b395898c65ce8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 17 Jan 2022 15:46:36 -0500 Subject: [PATCH 598/967] get lua version from luarocks itself --- pre_commit/languages/lua.py | 108 ++++++++---------------------------- tests/languages/lua_test.py | 55 ------------------ tests/repository_test.py | 10 ++-- 3 files changed, 28 insertions(+), 145 deletions(-) delete mode 100644 tests/languages/lua_test.py diff --git a/pre_commit/languages/lua.py b/pre_commit/languages/lua.py index ae322279b..f69993712 100644 --- a/pre_commit/languages/lua.py +++ b/pre_commit/languages/lua.py @@ -1,6 +1,6 @@ import contextlib import os -import re +import sys from typing import Generator from typing import Sequence from typing import Tuple @@ -11,7 +11,6 @@ from pre_commit.envcontext import Var from pre_commit.hook import Hook from pre_commit.languages import helpers -from pre_commit.parse_shebang import find_executable from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output @@ -21,95 +20,38 @@ healthy = helpers.basic_healthy -def _find_lua(language_version: str) -> str: # pragma: win32 no cover - """Find a lua executable. - - Lua doesn't always have a plain `lua` executable. - Some OS vendors will ship the binary as `lua#.#` (e.g., lua5.3) - so discovery is needed to find a valid executable. - """ - if language_version == C.DEFAULT: - choices = ['lua'] - for path in os.environ.get('PATH', '').split(os.pathsep): - try: - candidates = os.listdir(path) - except OSError: - # Invalid path on PATH or lacking permissions. - continue - - for candidate in candidates: - # The Lua executable might look like `lua#.#` or `lua-#.#`. - if re.search(r'^lua[-]?\d+\.\d+', candidate): - choices.append(candidate) - else: - # Prefer version specific executables first if available. - # This should avoid the corner case where a user requests a language - # version, gets a `lua` executable, but that executable is actually - # for a different version and package.path would patch LUA_PATH - # incorrectly. - choices = [f'lua{language_version}', 'lua-{language_version}', 'lua'] - - found_exes = [exe for exe in choices if find_executable(exe)] - if found_exes: - return found_exes[0] - - raise ValueError( - 'No lua executable found on the system paths ' - f'for {language_version} version.', - ) +def _get_lua_version() -> str: # pragma: win32 no cover + """Get the Lua version used in file paths.""" + _, stdout, _ = cmd_output('luarocks', 'config', '--lua-ver') + return stdout.strip() -def _get_lua_path_version( - lua_executable: str, -) -> str: # pragma: win32 no cover - """Get the Lua version used in file paths.""" - # This could sniff out from _VERSION, but checking package.path should - # provide an answer for *exactly* where lua is looking for packages. - _, stdout, _ = cmd_output(lua_executable, '-e', 'print(package.path)') - sep = os.sep if os.name != 'nt' else os.sep * 2 - match = re.search(fr'{sep}lua{sep}(.*?){sep}', stdout) - if match: - return match[1] - - raise ValueError('Cannot determine lua version for file paths.') - - -def get_env_patch( - env: str, language_version: str, -) -> PatchesT: # pragma: win32 no cover - lua = _find_lua(language_version) - version = _get_lua_path_version(lua) +def get_env_patch(d: str) -> PatchesT: # pragma: win32 no cover + version = _get_lua_version() + so_ext = 'dll' if sys.platform == 'win32' else 'so' return ( - ('PATH', (os.path.join(env, 'bin'), os.pathsep, Var('PATH'))), + ('PATH', (os.path.join(d, 'bin'), os.pathsep, Var('PATH'))), ( 'LUA_PATH', ( - os.path.join(env, 'share', 'lua', version, '?.lua;'), - os.path.join(env, 'share', 'lua', version, '?', 'init.lua;;'), + os.path.join(d, 'share', 'lua', version, '?.lua;'), + os.path.join(d, 'share', 'lua', version, '?', 'init.lua;;'), ), ), ( - 'LUA_CPATH', ( - os.path.join(env, 'lib', 'lua', version, '?.so;;'), - ), + 'LUA_CPATH', + (os.path.join(d, 'lib', 'lua', version, f'?.{so_ext};;'),), ), ) -def _envdir(prefix: Prefix, version: str) -> str: # pragma: win32 no cover - directory = helpers.environment_dir(ENVIRONMENT_DIR, version) +def _envdir(prefix: Prefix) -> str: # pragma: win32 no cover + directory = helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT) return prefix.path(directory) @contextlib.contextmanager # pragma: win32 no cover -def in_env( - prefix: Prefix, - language_version: str, -) -> Generator[None, None, None]: - with envcontext( - get_env_patch( - _envdir(prefix, language_version), language_version, - ), - ): +def in_env(prefix: Prefix) -> Generator[None, None, None]: + with envcontext(get_env_patch(_envdir(prefix))): yield @@ -120,19 +62,17 @@ def install_environment( ) -> None: # pragma: win32 no cover helpers.assert_version_default('lua', version) - envdir = _envdir(prefix, version) + envdir = _envdir(prefix) with clean_path_on_failure(envdir): - with in_env(prefix, version): + with in_env(prefix): # luarocks doesn't bootstrap a tree prior to installing # so ensure the directory exists. os.makedirs(envdir, exist_ok=True) - make_cmd = ['luarocks', '--tree', envdir, 'make'] - # Older luarocks (e.g., 2.4.2) expect the rockspec as an argument. - filenames = prefix.star('.rockspec') - make_cmd.extend(filenames[:1]) - - helpers.run_setup_cmd(prefix, tuple(make_cmd)) + # Older luarocks (e.g., 2.4.2) expect the rockspec as an arg + for rockspec in prefix.star('.rockspec'): + make_cmd = ('luarocks', '--tree', envdir, 'make', rockspec) + helpers.run_setup_cmd(prefix, make_cmd) # luarocks can't install multiple packages at once # so install them individually. @@ -146,5 +86,5 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: # pragma: win32 no cover - with in_env(hook.prefix, hook.language_version): + with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/tests/languages/lua_test.py b/tests/languages/lua_test.py deleted file mode 100644 index fba23b22f..000000000 --- a/tests/languages/lua_test.py +++ /dev/null @@ -1,55 +0,0 @@ -import os -from unittest import mock - -import pytest - -import pre_commit.constants as C -from pre_commit.languages import lua -from testing.util import xfailif_windows - - -@pytest.mark.parametrize( - 'lua_name', ('lua', 'lua5.4', 'lua-5.4', 'lua5.4.exe'), -) -def test_find_lua(tmp_path, lua_name): - """The language support can find common lua executable names.""" - lua_file = tmp_path / lua_name - lua_file.touch(0o555) - with mock.patch.dict(os.environ, {'PATH': str(tmp_path)}): - lua_executable = lua._find_lua(C.DEFAULT) - assert lua_name in lua_executable - - -def test_find_lua_language_version(tmp_path): - """Language discovery can find a specific version.""" - lua_file = tmp_path / 'lua5.99' - lua_file.touch(0o555) - with mock.patch.dict(os.environ, {'PATH': str(tmp_path)}): - lua_executable = lua._find_lua('5.99') - assert 'lua5.99' in lua_executable - - -@pytest.mark.parametrize( - ('invalid', 'mode'), - ( - ('foobar', 0o555), - ('luac', 0o555), - # Windows doesn't respect the executable checking. - pytest.param('lua5.4', 0o444, marks=xfailif_windows), - ), -) -def test_find_lua_fail(tmp_path, invalid, mode): - """No lua executable on the system will fail.""" - non_lua_file = tmp_path / invalid - non_lua_file.touch(mode) - with mock.patch.dict(os.environ, {'PATH': str(tmp_path)}): - with pytest.raises(ValueError): - lua._find_lua(C.DEFAULT) - - -@mock.patch.object(lua, 'cmd_output') -def test_bad_package_path(mock_cmd_output): - """A package path missing path info returns an unknown version.""" - mock_cmd_output.return_value = (0, '', '') - with pytest.raises(ValueError): - lua._get_lua_path_version('lua') diff --git a/tests/repository_test.py b/tests/repository_test.py index 5f5e17e55..8569ba96c 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -17,7 +17,6 @@ from pre_commit.hook import Hook from pre_commit.languages import golang from pre_commit.languages import helpers -from pre_commit.languages import lua from pre_commit.languages import node from pre_commit.languages import python from pre_commit.languages import ruby @@ -1142,18 +1141,17 @@ def test_lua_hook(tempdir_factory, store): @skipif_cant_run_lua # pragma: win32 no cover def test_local_lua_additional_dependencies(store): - lua_entry = lua._find_lua(C.DEFAULT) config = { 'repo': 'local', 'hooks': [{ 'id': 'local-lua', 'name': 'local-lua', - 'entry': lua_entry, + 'entry': 'luacheck --version', 'language': 'lua', - 'args': ['-e', 'require "inspect"; print("hello world")'], - 'additional_dependencies': ['inspect'], + 'additional_dependencies': ['luacheck'], }], } hook = _get_hook(config, store, 'local-lua') ret, out = _hook_run(hook, (), color=False) - assert (ret, _norm_out(out)) == (0, b'hello world\n') + assert b'Luacheck' in out + assert ret == 0 From d3bdf1403d92f8cf2dc77bd99a5da42f0a6cef17 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 18 Jan 2022 12:59:39 -0500 Subject: [PATCH 599/967] v2.17.0 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ setup.cfg | 2 +- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 49eab3fa3..66b50a482 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.16.0 + rev: v2.17.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index 55f46d9a0..d0cccc6da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,36 @@ +2.17.0 - 2022-01-18 +=================== + +### Features +- add warnings for regexes containing `[\\/]`. + - #2151 issue by @sanjioh. + - #2154 PR by @kuviokelluja. +- upgrade supported ruby versions. + - #2205 PR by @jalessio. +- allow `language: conda` to use `mamba` or `micromamba` via + `PRE_COMMIT_USE_MAMBA=1` or `PRE_COMMIT_USE_MICROMAMBA=1` respectively. + - #2204 issue by @janjagusch. + - #2207 PR by @xhochy. +- display `git --version` in error report. + - #2210 PR by @asottile. +- add `language: lua` as a supported language. + - #2158 PR by @mblayman. + +### Fixes +- temporarily add `setuptools` to the zipapp. + - #2122 issue by @andreoliwa. + - a737d5f commit by @asottile. +- use `go install` instead of `go get` for go 1.18+ support. + - #2161 PR by @schmir. +- fix `language: r` with a local renv and `RENV_PROJECT` set. + - #2170 PR by @lorenzwalthert. +- forbid overriding `entry` in `language: meta` hooks which breaks them. + - #2180 issue by @DanKaplanSES. + - #2181 PR by @asottile. +- always use `#!/bin/sh` on windows for hook script. + - #2182 issue by @hushigome-visco. + - #2187 PR by @asottile. + 2.16.0 - 2021-11-30 =================== diff --git a/setup.cfg b/setup.cfg index 02669c702..ef55b7cdc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.16.0 +version = 2.17.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 04de6a2e57ffed4660918f8f480eaf50f98e3c94 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 18 Jan 2022 17:36:17 -0500 Subject: [PATCH 600/967] drop python 3.6 support python 3.6 reached end of life on 2021-12-23 --- .pre-commit-config.yaml | 5 +- azure-pipelines.yml | 2 +- pre_commit/__main__.py | 2 + pre_commit/clientlib.py | 26 ++++----- pre_commit/color.py | 2 + pre_commit/commands/autoupdate.py | 22 ++++---- pre_commit/commands/clean.py | 2 + pre_commit/commands/gc.py | 11 ++-- pre_commit/commands/hook_impl.py | 30 +++++------ pre_commit/commands/init_templatedir.py | 2 + pre_commit/commands/install_uninstall.py | 12 ++--- pre_commit/commands/migrate_config.py | 2 + pre_commit/commands/run.py | 26 +++++---- pre_commit/commands/sample_config.py | 1 + pre_commit/commands/try_repo.py | 6 +-- pre_commit/constants.py | 2 + pre_commit/envcontext.py | 5 +- pre_commit/error_handler.py | 2 + pre_commit/errors.py | 3 ++ pre_commit/file_lock.py | 5 +- pre_commit/git.py | 24 ++++----- pre_commit/hook.py | 10 ++-- pre_commit/languages/all.py | 8 +-- pre_commit/languages/conda.py | 5 +- pre_commit/languages/coursier.py | 5 +- pre_commit/languages/dart.py | 7 +-- pre_commit/languages/docker.py | 11 ++-- pre_commit/languages/docker_image.py | 5 +- pre_commit/languages/dotnet.py | 5 +- pre_commit/languages/fail.py | 5 +- pre_commit/languages/golang.py | 5 +- pre_commit/languages/helpers.py | 19 ++++--- pre_commit/languages/lua.py | 5 +- pre_commit/languages/node.py | 5 +- pre_commit/languages/perl.py | 5 +- pre_commit/languages/pygrep.py | 8 +-- pre_commit/languages/python.py | 17 +++--- pre_commit/languages/r.py | 7 +-- pre_commit/languages/ruby.py | 5 +- pre_commit/languages/rust.py | 10 ++-- pre_commit/languages/script.py | 5 +- pre_commit/languages/swift.py | 5 +- pre_commit/languages/system.py | 5 +- pre_commit/logging_handler.py | 2 + pre_commit/main.py | 10 ++-- pre_commit/meta_hooks/check_hooks_apply.py | 5 +- .../meta_hooks/check_useless_excludes.py | 5 +- pre_commit/meta_hooks/identity.py | 5 +- pre_commit/output.py | 9 ++-- pre_commit/parse_shebang.py | 16 +++--- pre_commit/prefix.py | 5 +- pre_commit/repository.py | 39 +++++++------- pre_commit/resources/empty_template_setup.py | 2 + pre_commit/staged_files_only.py | 2 + pre_commit/store.py | 17 +++--- pre_commit/util.py | 54 ++++++++----------- pre_commit/xargs.py | 21 ++++---- setup.cfg | 4 +- setup.py | 2 + testing/auto_namedtuple.py | 2 + testing/fixtures.py | 2 + testing/gen-languages-all | 2 + testing/make-archives | 5 +- testing/resources/python_hooks_repo/foo.py | 2 + testing/resources/python_hooks_repo/setup.py | 2 + .../resources/python_venv_hooks_repo/foo.py | 2 + .../resources/python_venv_hooks_repo/setup.py | 2 + testing/util.py | 2 + testing/zipapp/Dockerfile | 4 +- testing/zipapp/entry | 7 ++- testing/zipapp/make | 2 + testing/zipapp/python | 7 ++- tests/clientlib_test.py | 2 + tests/color_test.py | 2 + tests/commands/autoupdate_test.py | 2 + tests/commands/clean_test.py | 2 + tests/commands/gc_test.py | 2 + tests/commands/hook_impl_test.py | 2 + tests/commands/init_templatedir_test.py | 2 + tests/commands/install_uninstall_test.py | 2 + tests/commands/migrate_config_test.py | 2 + tests/commands/run_test.py | 2 + tests/commands/sample_config_test.py | 2 + tests/commands/try_repo_test.py | 2 + tests/conftest.py | 2 + tests/envcontext_test.py | 2 + tests/error_handler_test.py | 2 + tests/git_test.py | 2 + tests/languages/conda_test.py | 2 + tests/languages/docker_test.py | 2 + tests/languages/golang_test.py | 2 + tests/languages/helpers_test.py | 2 + tests/languages/node_test.py | 2 + tests/languages/pygrep_test.py | 2 + tests/languages/python_test.py | 10 ++-- tests/languages/r_test.py | 2 + tests/languages/ruby_test.py | 2 + tests/logging_handler_test.py | 2 + tests/main_test.py | 2 + tests/meta_hooks/check_hooks_apply_test.py | 2 + .../meta_hooks/check_useless_excludes_test.py | 2 + tests/meta_hooks/identity_test.py | 2 + tests/output_test.py | 2 + tests/parse_shebang_test.py | 2 + tests/prefix_test.py | 2 + tests/repository_test.py | 7 +-- tests/staged_files_only_test.py | 2 + tests/store_test.py | 2 + tests/util_test.py | 2 + tests/xargs_test.py | 5 +- tox.ini | 2 +- 111 files changed, 401 insertions(+), 286 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 66b50a482..5103e0bef 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,12 +28,13 @@ repos: rev: v2.31.0 hooks: - id: pyupgrade - args: [--py36-plus] + args: [--py37-plus] - repo: https://github.com/asottile/reorder_python_imports rev: v2.6.0 hooks: - id: reorder-python-imports - args: [--py3-plus] + args: [--py37-plus, --add-import, 'from __future__ import annotations'] + exclude: ^testing/resources/python3_hooks_repo/ - repo: https://github.com/asottile/add-trailing-comma rev: v2.2.1 hooks: diff --git a/azure-pipelines.yml b/azure-pipelines.yml index d8cbd11d5..d3336a46a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -50,7 +50,7 @@ jobs: displayName: install R - template: job--python-tox.yml@asottile parameters: - toxenvs: [pypy3, py36, py37, py38, py39] + toxenvs: [py37, py38, py39] os: linux pre_test: - task: UseRubyVersion@0 diff --git a/pre_commit/__main__.py b/pre_commit/__main__.py index 83bd93ca4..bda61eecc 100644 --- a/pre_commit/__main__.py +++ b/pre_commit/__main__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pre_commit.main import main diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 47ebd54f7..1fcce4eaf 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import argparse import functools import logging @@ -5,8 +7,6 @@ import shlex import sys from typing import Any -from typing import Dict -from typing import Optional from typing import Sequence import cfgv @@ -95,7 +95,7 @@ class InvalidManifestError(FatalError): ) -def validate_manifest_main(argv: Optional[Sequence[str]] = None) -> int: +def validate_manifest_main(argv: Sequence[str] | None = None) -> int: parser = _make_argparser('Manifest filenames.') args = parser.parse_args(argv) @@ -116,7 +116,7 @@ def validate_manifest_main(argv: Optional[Sequence[str]] = None) -> int: # should inherit from cfgv.Conditional if sha support is dropped class WarnMutableRev(cfgv.ConditionalOptional): - def check(self, dct: Dict[str, Any]) -> None: + def check(self, dct: dict[str, Any]) -> None: super().check(dct) if self.key in dct: @@ -135,7 +135,7 @@ def check(self, dct: Dict[str, Any]) -> None: class OptionalSensibleRegexAtHook(cfgv.OptionalNoDefault): - def check(self, dct: Dict[str, Any]) -> None: + def check(self, dct: dict[str, Any]) -> None: super().check(dct) if '/*' in dct.get(self.key, ''): @@ -154,7 +154,7 @@ def check(self, dct: Dict[str, Any]) -> None: class OptionalSensibleRegexAtTop(cfgv.OptionalNoDefault): - def check(self, dct: Dict[str, Any]) -> None: + def check(self, dct: dict[str, Any]) -> None: super().check(dct) if '/*' in dct.get(self.key, ''): @@ -183,7 +183,7 @@ def _cond(key: str) -> cfgv.Conditional: ensure_absent=True, ) - def check(self, dct: Dict[str, Any]) -> None: + def check(self, dct: dict[str, Any]) -> None: if dct.get('repo') in {LOCAL, META}: self._cond('rev').check(dct) self._cond('sha').check(dct) @@ -194,7 +194,7 @@ def check(self, dct: Dict[str, Any]) -> None: else: self._cond('rev').check(dct) - def apply_default(self, dct: Dict[str, Any]) -> None: + def apply_default(self, dct: dict[str, Any]) -> None: if 'sha' in dct: dct['rev'] = dct.pop('sha') @@ -212,7 +212,7 @@ def _entry(modname: str) -> str: def warn_unknown_keys_root( extra: Sequence[str], orig_keys: Sequence[str], - dct: Dict[str, str], + dct: dict[str, str], ) -> None: logger.warning(f'Unexpected key(s) present at root: {", ".join(extra)}') @@ -220,7 +220,7 @@ def warn_unknown_keys_root( def warn_unknown_keys_repo( extra: Sequence[str], orig_keys: Sequence[str], - dct: Dict[str, str], + dct: dict[str, str], ) -> None: logger.warning( f'Unexpected key(s) present on {dct["repo"]}: {", ".join(extra)}', @@ -253,7 +253,7 @@ def warn_unknown_keys_repo( class NotAllowed(cfgv.OptionalNoDefault): - def check(self, dct: Dict[str, Any]) -> None: + def check(self, dct: dict[str, Any]) -> None: if self.key in dct: raise cfgv.ValidationError(f'{self.key!r} cannot be overridden') @@ -377,7 +377,7 @@ class InvalidConfigError(FatalError): pass -def ordered_load_normalize_legacy_config(contents: str) -> Dict[str, Any]: +def ordered_load_normalize_legacy_config(contents: str) -> dict[str, Any]: data = yaml_load(contents) if isinstance(data, list): logger.warning( @@ -398,7 +398,7 @@ def ordered_load_normalize_legacy_config(contents: str) -> Dict[str, Any]: ) -def validate_config_main(argv: Optional[Sequence[str]] = None) -> int: +def validate_config_main(argv: Sequence[str] | None = None) -> int: parser = _make_argparser('Config filenames.') args = parser.parse_args(argv) diff --git a/pre_commit/color.py b/pre_commit/color.py index 4ddfdf5b3..2d6f248b6 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import argparse import os import sys diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 5cb978e92..938c22461 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -1,12 +1,10 @@ +from __future__ import annotations + import os.path import re from typing import Any -from typing import Dict -from typing import List from typing import NamedTuple -from typing import Optional from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit import git @@ -29,13 +27,13 @@ class RevInfo(NamedTuple): repo: str rev: str - frozen: Optional[str] + frozen: str | None @classmethod - def from_config(cls, config: Dict[str, Any]) -> 'RevInfo': + def from_config(cls, config: dict[str, Any]) -> RevInfo: return cls(config['repo'], config['rev'], None) - def update(self, tags_only: bool, freeze: bool) -> 'RevInfo': + def update(self, tags_only: bool, freeze: bool) -> RevInfo: git_cmd = ('git', *git.NO_FS_MONITOR) if tags_only: @@ -76,7 +74,7 @@ class RepositoryCannotBeUpdatedError(RuntimeError): def _check_hooks_still_exist_at_rev( - repo_config: Dict[str, Any], + repo_config: dict[str, Any], info: RevInfo, store: Store, ) -> None: @@ -101,9 +99,9 @@ def _check_hooks_still_exist_at_rev( def _original_lines( path: str, - rev_infos: List[Optional[RevInfo]], + rev_infos: list[RevInfo | None], retry: bool = False, -) -> Tuple[List[str], List[int]]: +) -> tuple[list[str], list[int]]: """detect `rev:` lines or reformat the file""" with open(path, newline='') as f: original = f.read() @@ -120,7 +118,7 @@ def _original_lines( return _original_lines(path, rev_infos, retry=True) -def _write_new_config(path: str, rev_infos: List[Optional[RevInfo]]) -> None: +def _write_new_config(path: str, rev_infos: list[RevInfo | None]) -> None: lines, idxs = _original_lines(path, rev_infos) for idx, rev_info in zip(idxs, rev_infos): @@ -152,7 +150,7 @@ def autoupdate( """Auto-update the pre-commit config to the latest versions of repos.""" migrate_config(config_file, quiet=True) retv = 0 - rev_infos: List[Optional[RevInfo]] = [] + rev_infos: list[RevInfo | None] = [] changed = False config = load_config(config_file) diff --git a/pre_commit/commands/clean.py b/pre_commit/commands/clean.py index 2be6c16a5..5119f6455 100644 --- a/pre_commit/commands/clean.py +++ b/pre_commit/commands/clean.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path from pre_commit import output diff --git a/pre_commit/commands/gc.py b/pre_commit/commands/gc.py index 7f6d31119..6892e097c 100644 --- a/pre_commit/commands/gc.py +++ b/pre_commit/commands/gc.py @@ -1,8 +1,7 @@ +from __future__ import annotations + import os.path from typing import Any -from typing import Dict -from typing import Set -from typing import Tuple import pre_commit.constants as C from pre_commit import output @@ -17,9 +16,9 @@ def _mark_used_repos( store: Store, - all_repos: Dict[Tuple[str, str], str], - unused_repos: Set[Tuple[str, str]], - repo: Dict[str, Any], + all_repos: dict[tuple[str, str], str], + unused_repos: set[tuple[str, str]], + repo: dict[str, Any], ) -> None: if repo['repo'] == META: return diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index 90bb33b8d..18e5e9f59 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -1,10 +1,10 @@ +from __future__ import annotations + import argparse import os.path import subprocess import sys -from typing import Optional from typing import Sequence -from typing import Tuple from pre_commit.commands.run import run from pre_commit.envcontext import envcontext @@ -18,7 +18,7 @@ def _run_legacy( hook_type: str, hook_dir: str, args: Sequence[str], -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: if os.environ.get('PRE_COMMIT_RUNNING_LEGACY'): raise SystemExit( f"bug: pre-commit's script is installed in migration mode\n" @@ -69,16 +69,16 @@ def _ns( color: bool, *, all_files: bool = False, - remote_branch: Optional[str] = None, - local_branch: Optional[str] = None, - from_ref: Optional[str] = None, - to_ref: Optional[str] = None, - remote_name: Optional[str] = None, - remote_url: Optional[str] = None, - commit_msg_filename: Optional[str] = None, - checkout_type: Optional[str] = None, - is_squash_merge: Optional[str] = None, - rewrite_command: Optional[str] = None, + remote_branch: str | None = None, + local_branch: str | None = None, + from_ref: str | None = None, + to_ref: str | None = None, + remote_name: str | None = None, + remote_url: str | None = None, + commit_msg_filename: str | None = None, + checkout_type: str | None = None, + is_squash_merge: str | None = None, + rewrite_command: str | None = None, ) -> argparse.Namespace: return argparse.Namespace( color=color, @@ -109,7 +109,7 @@ def _pre_push_ns( color: bool, args: Sequence[str], stdin: bytes, -) -> Optional[argparse.Namespace]: +) -> argparse.Namespace | None: remote_name = args[0] remote_url = args[1] @@ -197,7 +197,7 @@ def _run_ns( color: bool, args: Sequence[str], stdin: bytes, -) -> Optional[argparse.Namespace]: +) -> argparse.Namespace | None: _check_args_length(hook_type, args) if hook_type == 'pre-push': return _pre_push_ns(color, args, stdin) diff --git a/pre_commit/commands/init_templatedir.py b/pre_commit/commands/init_templatedir.py index 5f17d9c12..004e8ccfc 100644 --- a/pre_commit/commands/init_templatedir.py +++ b/pre_commit/commands/init_templatedir.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging import os.path from typing import Sequence diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 50c644320..cb2aaa5bf 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -1,11 +1,11 @@ +from __future__ import annotations + import logging import os.path import shlex import shutil import sys -from typing import Optional from typing import Sequence -from typing import Tuple from pre_commit import git from pre_commit import output @@ -34,8 +34,8 @@ def _hook_paths( hook_type: str, - git_dir: Optional[str] = None, -) -> Tuple[str, str]: + git_dir: str | None = None, +) -> tuple[str, str]: git_dir = git_dir if git_dir is not None else git.get_git_dir() pth = os.path.join(git_dir, 'hooks', hook_type) return pth, f'{pth}.legacy' @@ -54,7 +54,7 @@ def _install_hook_script( hook_type: str, overwrite: bool = False, skip_on_missing_config: bool = False, - git_dir: Optional[str] = None, + git_dir: str | None = None, ) -> None: hook_path, legacy_path = _hook_paths(hook_type, git_dir=git_dir) @@ -107,7 +107,7 @@ def install( overwrite: bool = False, hooks: bool = False, skip_on_missing_config: bool = False, - git_dir: Optional[str] = None, + git_dir: str | None = None, ) -> int: if git_dir is None and git.has_core_hookpaths_set(): logger.error( diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index fef14cd39..c3d0a509f 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import re import textwrap diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index f8ced0f9b..37f989b57 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import argparse import contextlib import functools @@ -9,12 +11,8 @@ import unicodedata from typing import Any from typing import Collection -from typing import Dict -from typing import List from typing import MutableMapping from typing import Sequence -from typing import Set -from typing import Tuple from identify.identify import tags_from_path @@ -62,7 +60,7 @@ def filter_by_include_exclude( names: Collection[str], include: str, exclude: str, -) -> List[str]: +) -> list[str]: include_re, exclude_re = re.compile(include), re.compile(exclude) return [ filename for filename in names @@ -76,7 +74,7 @@ def __init__(self, filenames: Collection[str]) -> None: self.filenames = [f for f in filenames if os.path.lexists(f)] @functools.lru_cache(maxsize=None) - def _types_for_file(self, filename: str) -> Set[str]: + def _types_for_file(self, filename: str) -> set[str]: return tags_from_path(filename) def by_types( @@ -85,7 +83,7 @@ def by_types( types: Collection[str], types_or: Collection[str], exclude_types: Collection[str], - ) -> List[str]: + ) -> list[str]: types = frozenset(types) types_or = frozenset(types_or) exclude_types = frozenset(exclude_types) @@ -100,7 +98,7 @@ def by_types( ret.append(filename) return ret - def filenames_for_hook(self, hook: Hook) -> Tuple[str, ...]: + def filenames_for_hook(self, hook: Hook) -> tuple[str, ...]: names = self.filenames names = filter_by_include_exclude(names, hook.files, hook.exclude) names = self.by_types( @@ -117,7 +115,7 @@ def from_config( filenames: Collection[str], include: str, exclude: str, - ) -> 'Classifier': + ) -> Classifier: # on windows we normalize all filenames to use forward slashes # this makes it easier to filter using the `files:` regex # this also makes improperly quoted shell-based hooks work better @@ -128,7 +126,7 @@ def from_config( return Classifier(filenames) -def _get_skips(environ: MutableMapping[str, str]) -> Set[str]: +def _get_skips(environ: MutableMapping[str, str]) -> set[str]: skips = environ.get('SKIP', '') return {skip.strip() for skip in skips.split(',') if skip.strip()} @@ -144,12 +142,12 @@ def _subtle_line(s: str, use_color: bool) -> None: def _run_single_hook( classifier: Classifier, hook: Hook, - skips: Set[str], + skips: set[str], cols: int, diff_before: bytes, verbose: bool, use_color: bool, -) -> Tuple[bool, bytes]: +) -> tuple[bool, bytes]: filenames = classifier.filenames_for_hook(hook) if hook.id in skips or hook.alias in skips: @@ -271,9 +269,9 @@ def _get_diff() -> bytes: def _run_hooks( - config: Dict[str, Any], + config: dict[str, Any], hooks: Sequence[Hook], - skips: Set[str], + skips: set[str], args: argparse.Namespace, ) -> int: """Actually run the hooks.""" diff --git a/pre_commit/commands/sample_config.py b/pre_commit/commands/sample_config.py index 64617c333..82a1617f8 100644 --- a/pre_commit/commands/sample_config.py +++ b/pre_commit/commands/sample_config.py @@ -2,6 +2,7 @@ # determine the latest revision? This adds ~200ms from my tests (and is # significantly faster than https:// or http://). For now, periodically # manually updating the revision is fine. +from __future__ import annotations SAMPLE_CONFIG = '''\ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index 4aee209c6..ef099f5e3 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -1,8 +1,8 @@ +from __future__ import annotations + import argparse import logging import os.path -from typing import Optional -from typing import Tuple import pre_commit.constants as C from pre_commit import git @@ -18,7 +18,7 @@ logger = logging.getLogger(__name__) -def _repo_ref(tmpdir: str, repo: str, ref: Optional[str]) -> Tuple[str, str]: +def _repo_ref(tmpdir: str, repo: str, ref: str | None) -> tuple[str, str]: # if `ref` is explicitly passed, use it if ref is not None: return repo, ref diff --git a/pre_commit/constants.py b/pre_commit/constants.py index d2f93636a..40127a052 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys if sys.version_info >= (3, 8): # pragma: >=3.8 cover diff --git a/pre_commit/envcontext.py b/pre_commit/envcontext.py index 92d975d09..4f5956016 100644 --- a/pre_commit/envcontext.py +++ b/pre_commit/envcontext.py @@ -1,10 +1,11 @@ +from __future__ import annotations + import contextlib import enum import os from typing import Generator from typing import MutableMapping from typing import NamedTuple -from typing import Optional from typing import Tuple from typing import Union @@ -32,7 +33,7 @@ def format_env(parts: SubstitutionT, env: MutableMapping[str, str]) -> str: @contextlib.contextmanager def envcontext( patch: PatchesT, - _env: Optional[MutableMapping[str, str]] = None, + _env: MutableMapping[str, str] | None = None, ) -> Generator[None, None, None]: """In this context, `os.environ` is modified according to `patch`. diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 7e74b9589..a6a7329e7 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import functools import os.path diff --git a/pre_commit/errors.py b/pre_commit/errors.py index f84d3f185..eac34faa6 100644 --- a/pre_commit/errors.py +++ b/pre_commit/errors.py @@ -1,2 +1,5 @@ +from __future__ import annotations + + class FatalError(RuntimeError): pass diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index 55a8eb29c..f67a58644 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import errno import sys @@ -20,13 +22,11 @@ def _locked( blocked_cb: Callable[[], None], ) -> Generator[None, None, None]: try: - # TODO: https://github.com/python/typeshed/pull/3607 msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region) except OSError: blocked_cb() while True: try: - # TODO: https://github.com/python/typeshed/pull/3607 msvcrt.locking(fileno, msvcrt.LK_LOCK, _region) except OSError as e: # Locking violation. Returned when the _LK_LOCK or _LK_RLCK @@ -45,7 +45,6 @@ def _locked( # The documentation however states: # "Regions should be locked only briefly and should be unlocked # before closing a file or exiting the program." - # TODO: https://github.com/python/typeshed/pull/3607 msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region) else: # pragma: win32 no cover import fcntl diff --git a/pre_commit/git.py b/pre_commit/git.py index e9ec60140..67499cdb8 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -1,11 +1,9 @@ +from __future__ import annotations + import logging import os.path import sys -from typing import Dict -from typing import List from typing import MutableMapping -from typing import Optional -from typing import Set from pre_commit.errors import FatalError from pre_commit.util import CalledProcessError @@ -18,7 +16,7 @@ NO_FS_MONITOR = ('-c', 'core.useBuiltinFSMonitor=false') -def zsplit(s: str) -> List[str]: +def zsplit(s: str) -> list[str]: s = s.strip('\0') if s: return s.split('\0') @@ -27,8 +25,8 @@ def zsplit(s: str) -> List[str]: def no_git_env( - _env: Optional[MutableMapping[str, str]] = None, -) -> Dict[str, str]: + _env: MutableMapping[str, str] | None = None, +) -> dict[str, str]: # Too many bugs dealing with environment variables and GIT: # https://github.com/pre-commit/pre-commit/issues/300 # In git 2.6.3 (maybe others), git exports GIT_WORK_TREE while running @@ -95,7 +93,7 @@ def is_in_merge_conflict() -> bool: ) -def parse_merge_msg_for_conflicts(merge_msg: bytes) -> List[str]: +def parse_merge_msg_for_conflicts(merge_msg: bytes) -> list[str]: # Conflicted files start with tabs return [ line.lstrip(b'#').strip().decode() @@ -105,7 +103,7 @@ def parse_merge_msg_for_conflicts(merge_msg: bytes) -> List[str]: ] -def get_conflicted_files() -> Set[str]: +def get_conflicted_files() -> set[str]: logger.info('Checking merge-conflict files only.') # Need to get the conflicted files from the MERGE_MSG because they could # have resolved the conflict by choosing one side or the other @@ -126,7 +124,7 @@ def get_conflicted_files() -> Set[str]: return set(merge_conflict_filenames) | set(merge_diff_filenames) -def get_staged_files(cwd: Optional[str] = None) -> List[str]: +def get_staged_files(cwd: str | None = None) -> list[str]: return zsplit( cmd_output( 'git', 'diff', '--staged', '--name-only', '--no-ext-diff', '-z', @@ -137,7 +135,7 @@ def get_staged_files(cwd: Optional[str] = None) -> List[str]: ) -def intent_to_add_files() -> List[str]: +def intent_to_add_files() -> list[str]: _, stdout, _ = cmd_output( 'git', 'status', '--ignore-submodules', '--porcelain', '-z', ) @@ -153,11 +151,11 @@ def intent_to_add_files() -> List[str]: return intent_to_add -def get_all_files() -> List[str]: +def get_all_files() -> list[str]: return zsplit(cmd_output('git', 'ls-files', '-z')[1]) -def get_changed_files(old: str, new: str) -> List[str]: +def get_changed_files(old: str, new: str) -> list[str]: diff_cmd = ('git', 'diff', '--name-only', '--no-ext-diff', '-z') try: _, out, _ = cmd_output(*diff_cmd, f'{old}...{new}') diff --git a/pre_commit/hook.py b/pre_commit/hook.py index 82e99c543..202abb358 100644 --- a/pre_commit/hook.py +++ b/pre_commit/hook.py @@ -1,10 +1,10 @@ +from __future__ import annotations + import logging import shlex from typing import Any -from typing import Dict from typing import NamedTuple from typing import Sequence -from typing import Tuple from pre_commit.prefix import Prefix @@ -38,11 +38,11 @@ class Hook(NamedTuple): verbose: bool @property - def cmd(self) -> Tuple[str, ...]: + def cmd(self) -> tuple[str, ...]: return (*shlex.split(self.entry), *self.args) @property - def install_key(self) -> Tuple[Prefix, str, str, Tuple[str, ...]]: + def install_key(self) -> tuple[Prefix, str, str, tuple[str, ...]]: return ( self.prefix, self.language, @@ -51,7 +51,7 @@ def install_key(self) -> Tuple[Prefix, str, str, Tuple[str, ...]]: ) @classmethod - def create(cls, src: str, prefix: Prefix, dct: Dict[str, Any]) -> 'Hook': + def create(cls, src: str, prefix: Prefix, dct: dict[str, Any]) -> Hook: # TODO: have cfgv do this (?) extra_keys = set(dct) - _KEYS if extra_keys: diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 0bcedd66d..cfcbf686b 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -1,8 +1,8 @@ +from __future__ import annotations + from typing import Callable from typing import NamedTuple -from typing import Optional from typing import Sequence -from typing import Tuple from pre_commit.hook import Hook from pre_commit.languages import conda @@ -30,7 +30,7 @@ class Language(NamedTuple): name: str # Use `None` for no installation / environment - ENVIRONMENT_DIR: Optional[str] + ENVIRONMENT_DIR: str | None # return a value to replace `'default` for `language_version` get_default_version: Callable[[], str] # return whether the environment is healthy (or should be rebuilt) @@ -38,7 +38,7 @@ class Language(NamedTuple): # install a repository for the given language and language_version install_environment: Callable[[Prefix, str, Sequence[str]], None] # execute a hook and return the exit code and output - run_hook: 'Callable[[Hook, Sequence[str], bool], Tuple[int, bytes]]' + run_hook: Callable[[Hook, Sequence[str], bool], tuple[int, bytes]] # TODO: back to modules + Protocol: https://github.com/python/mypy/issues/5018 diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index 97e2f69eb..88ac53f33 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import contextlib import os from typing import Generator from typing import Sequence -from typing import Tuple from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT @@ -86,7 +87,7 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: # TODO: Some rare commands need to be run using `conda run` but mostly we # can run them without which is much quicker and produces a better # output. diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index 2841467fc..e47f9c873 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import contextlib import os from typing import Generator from typing import Sequence -from typing import Tuple from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT @@ -66,6 +67,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: # pragma: win32 no cover +) -> tuple[int, bytes]: # pragma: win32 no cover with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py index 16e755461..65135f80a 100644 --- a/pre_commit/languages/dart.py +++ b/pre_commit/languages/dart.py @@ -1,10 +1,11 @@ +from __future__ import annotations + import contextlib import os.path import shutil import tempfile from typing import Generator from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit.envcontext import envcontext @@ -76,7 +77,7 @@ def _install_dir(prefix_p: Prefix, pub_cache: str) -> None: with tempfile.TemporaryDirectory() as dep_tmp: dep, _, version = dep_s.partition(':') if version: - dep_cmd: Tuple[str, ...] = (dep, '--version', version) + dep_cmd: tuple[str, ...] = (dep, '--version', version) else: dep_cmd = (dep,) @@ -104,6 +105,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 644d8d293..af1860c5b 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import hashlib import json import os from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit.hook import Hook @@ -76,7 +77,7 @@ def build_docker_image( *, pull: bool, ) -> None: # pragma: win32 no cover - cmd: Tuple[str, ...] = ( + cmd: tuple[str, ...] = ( 'docker', 'build', '--tag', docker_tag(prefix), '--label', PRE_COMMIT_LABEL, @@ -105,14 +106,14 @@ def install_environment( os.mkdir(directory) -def get_docker_user() -> Tuple[str, ...]: # pragma: win32 no cover +def get_docker_user() -> tuple[str, ...]: # pragma: win32 no cover try: return ('-u', f'{os.getuid()}:{os.getgid()}') except AttributeError: return () -def docker_cmd() -> Tuple[str, ...]: # pragma: win32 no cover +def docker_cmd() -> tuple[str, ...]: # pragma: win32 no cover return ( 'docker', 'run', '--rm', @@ -129,7 +130,7 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: # pragma: win32 no cover +) -> tuple[int, bytes]: # pragma: win32 no cover # Rebuild the docker image in case it has gone missing, as many people do # automated cleanup of docker images. build_docker_image(hook.prefix, pull=False) diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index 311d1277d..ccc1d6782 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -1,5 +1,6 @@ +from __future__ import annotations + from typing import Sequence -from typing import Tuple from pre_commit.hook import Hook from pre_commit.languages import helpers @@ -15,6 +16,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: # pragma: win32 no cover +) -> tuple[int, bytes]: # pragma: win32 no cover cmd = docker_cmd() + hook.cmd return helpers.run_xargs(hook, cmd, file_args, color=color) diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index 094d2f1ce..a16e7f077 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import contextlib import os.path from typing import Generator from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit.envcontext import envcontext @@ -84,6 +85,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index d2b02d23e..4cb95af5a 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -1,5 +1,6 @@ +from __future__ import annotations + from typing import Sequence -from typing import Tuple from pre_commit.hook import Hook from pre_commit.languages import helpers @@ -14,7 +15,7 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: out = f'{hook.entry}\n\n'.encode() out += b'\n'.join(f.encode() for f in file_args) + b'\n' return 1, out diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 10ebc6280..759c26849 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -1,9 +1,10 @@ +from __future__ import annotations + import contextlib import os.path import sys from typing import Generator from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit import git @@ -95,6 +96,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 276ce1611..dd219ffa1 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -1,13 +1,12 @@ +from __future__ import annotations + import multiprocessing import os import random import re from typing import Any -from typing import List -from typing import Optional from typing import overload from typing import Sequence -from typing import Tuple from typing import TYPE_CHECKING import pre_commit.constants as C @@ -32,7 +31,7 @@ def exe_exists(exe: str) -> bool: homedir = os.path.expanduser('~') try: - common: Optional[str] = os.path.commonpath((found, homedir)) + common: str | None = os.path.commonpath((found, homedir)) except ValueError: # on windows, different drives raises ValueError common = None @@ -48,7 +47,7 @@ def exe_exists(exe: str) -> bool: ) -def run_setup_cmd(prefix: Prefix, cmd: Tuple[str, ...], **kwargs: Any) -> None: +def run_setup_cmd(prefix: Prefix, cmd: tuple[str, ...], **kwargs: Any) -> None: cmd_output_b(*cmd, cwd=prefix.prefix_dir, **kwargs) @@ -58,7 +57,7 @@ def environment_dir(d: None, language_version: str) -> None: ... def environment_dir(d: str, language_version: str) -> str: ... -def environment_dir(d: Optional[str], language_version: str) -> Optional[str]: +def environment_dir(d: str | None, language_version: str) -> str | None: if d is None: return None else: @@ -95,7 +94,7 @@ def no_install( prefix: Prefix, version: str, additional_dependencies: Sequence[str], -) -> 'NoReturn': +) -> NoReturn: raise AssertionError('This type is not installable') @@ -113,7 +112,7 @@ def target_concurrency(hook: Hook) -> int: return 1 -def _shuffled(seq: Sequence[str]) -> List[str]: +def _shuffled(seq: Sequence[str]) -> list[str]: """Deterministically shuffle""" fixed_random = random.Random() fixed_random.seed(FIXED_RANDOM_SEED, version=1) @@ -125,10 +124,10 @@ def _shuffled(seq: Sequence[str]) -> List[str]: def run_xargs( hook: Hook, - cmd: Tuple[str, ...], + cmd: tuple[str, ...], file_args: Sequence[str], **kwargs: Any, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: # Shuffle the files so that they more evenly fill out the xargs partitions, # but do it deterministically in case a hook cares about ordering. file_args = _shuffled(file_args) diff --git a/pre_commit/languages/lua.py b/pre_commit/languages/lua.py index f69993712..38bdf54b8 100644 --- a/pre_commit/languages/lua.py +++ b/pre_commit/languages/lua.py @@ -1,9 +1,10 @@ +from __future__ import annotations + import contextlib import os import sys from typing import Generator from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit.envcontext import envcontext @@ -85,6 +86,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: # pragma: win32 no cover +) -> tuple[int, bytes]: # pragma: win32 no cover with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 8dc4e8ba9..b084e8f89 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -1,10 +1,11 @@ +from __future__ import annotations + import contextlib import functools import os import sys from typing import Generator from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit.envcontext import envcontext @@ -122,6 +123,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py index bbf550494..0eee258d7 100644 --- a/pre_commit/languages/perl.py +++ b/pre_commit/languages/perl.py @@ -1,9 +1,10 @@ +from __future__ import annotations + import contextlib import os import shlex from typing import Generator from typing import Sequence -from typing import Tuple from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT @@ -62,6 +63,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index a713c3fb5..f2758c588 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -1,11 +1,11 @@ +from __future__ import annotations + import argparse import re import sys from typing import NamedTuple -from typing import Optional from typing import Pattern from typing import Sequence -from typing import Tuple from pre_commit import output from pre_commit.hook import Hook @@ -90,12 +90,12 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: exe = (sys.executable, '-m', __name__) + tuple(hook.args) + (hook.entry,) return xargs(exe, file_args, color=color) -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser( description=( 'grep-like finder using python regexes. Unlike grep, this tool ' diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index faa602977..668ba3587 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -1,12 +1,11 @@ +from __future__ import annotations + import contextlib import functools import os import sys -from typing import Dict from typing import Generator -from typing import Optional from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit.envcontext import envcontext @@ -35,7 +34,7 @@ def _version_info(exe: str) -> str: return f'<>' -def _read_pyvenv_cfg(filename: str) -> Dict[str, str]: +def _read_pyvenv_cfg(filename: str) -> dict[str, str]: ret = {} with open(filename, encoding='UTF-8') as f: for line in f: @@ -65,7 +64,7 @@ def get_env_patch(venv: str) -> PatchesT: def _find_by_py_launcher( version: str, -) -> Optional[str]: # pragma: no cover (windows only) +) -> str | None: # pragma: no cover (windows only) if version.startswith('python'): num = version[len('python'):] cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)') @@ -77,8 +76,8 @@ def _find_by_py_launcher( return None -def _find_by_sys_executable() -> Optional[str]: - def _norm(path: str) -> Optional[str]: +def _find_by_sys_executable() -> str | None: + def _norm(path: str) -> str | None: _, exe = os.path.split(path.lower()) exe, _, _ = exe.partition('.exe') if exe not in {'python', 'pythonw'} and find_executable(exe): @@ -133,7 +132,7 @@ def _sys_executable_matches(version: str) -> bool: return sys.version_info[:len(info)] == info -def norm_version(version: str) -> Optional[str]: +def norm_version(version: str) -> str | None: if version == C.DEFAULT: # use virtualenv's default return None elif _sys_executable_matches(version): # virtualenv defaults to our exe @@ -209,6 +208,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index e034e3904..2ad8c411b 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -1,10 +1,11 @@ +from __future__ import annotations + import contextlib import os import shlex import shutil from typing import Generator from typing import Sequence -from typing import Tuple from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT @@ -80,7 +81,7 @@ def _entry_validate(entry: Sequence[str]) -> None: ) -def _cmd_from_hook(hook: Hook) -> Tuple[str, ...]: +def _cmd_from_hook(hook: Hook) -> tuple[str, ...]: entry = shlex.split(hook.entry) _entry_validate(entry) @@ -148,7 +149,7 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: with in_env(hook.prefix, hook.language_version): return helpers.run_xargs( hook, _cmd_from_hook(hook), file_args, color=color, diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 81bc95436..ae6449270 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import functools import os.path @@ -5,7 +7,6 @@ import tarfile from typing import Generator from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit.envcontext import envcontext @@ -146,6 +147,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 7ea3f5406..39e36281c 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -1,9 +1,9 @@ +from __future__ import annotations + import contextlib import os.path from typing import Generator from typing import Sequence -from typing import Set -from typing import Tuple import toml @@ -39,7 +39,7 @@ def in_env(prefix: Prefix) -> Generator[None, None, None]: def _add_dependencies( cargo_toml_path: str, - additional_dependencies: Set[str], + additional_dependencies: set[str], ) -> None: with open(cargo_toml_path, 'r+') as f: cargo_toml = toml.load(f) @@ -81,7 +81,7 @@ def install_environment( _add_dependencies(prefix.path('Cargo.toml'), lib_deps) with clean_path_on_failure(directory): - packages_to_install: Set[Tuple[str, ...]] = {('--path', '.')} + packages_to_install: set[tuple[str, ...]] = {('--path', '.')} for cli_dep in cli_deps: cli_dep = cli_dep[len('cli:'):] package, _, version = cli_dep.partition(':') @@ -101,6 +101,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index a5e1365c0..2844b5e5d 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -1,5 +1,6 @@ +from __future__ import annotations + from typing import Sequence -from typing import Tuple from pre_commit.hook import Hook from pre_commit.languages import helpers @@ -14,6 +15,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: cmd = (hook.prefix.path(hook.cmd[0]), *hook.cmd[1:]) return helpers.run_xargs(hook, cmd, file_args, color=color) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 66aadc8b2..c6309531e 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import contextlib import os from typing import Generator from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit.envcontext import envcontext @@ -59,6 +60,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: # pragma: win32 no cover +) -> tuple[int, bytes]: # pragma: win32 no cover with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 139f45d13..9846c98ba 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -1,5 +1,6 @@ +from __future__ import annotations + from typing import Sequence -from typing import Tuple from pre_commit.hook import Hook from pre_commit.languages import helpers @@ -15,5 +16,5 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/logging_handler.py b/pre_commit/logging_handler.py index ba05295da..1b68fc7d6 100644 --- a/pre_commit/logging_handler.py +++ b/pre_commit/logging_handler.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import logging from typing import Generator diff --git a/pre_commit/main.py b/pre_commit/main.py index f1e8d03db..7ab9515c7 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -1,11 +1,11 @@ +from __future__ import annotations + import argparse import logging import os import sys from typing import Any -from typing import Optional from typing import Sequence -from typing import Union import pre_commit.constants as C from pre_commit import git @@ -55,8 +55,8 @@ def __call__( self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, - values: Union[str, Sequence[str], None], - option_string: Optional[str] = None, + values: str | Sequence[str] | None, + option_string: str | None = None, ) -> None: if not self.appended: setattr(namespace, self.dest, []) @@ -175,7 +175,7 @@ def _adjust_args_and_chdir(args: argparse.Namespace) -> None: args.repo = os.path.relpath(args.repo) -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: argv = argv if argv is not None else sys.argv[1:] parser = argparse.ArgumentParser(prog='pre-commit') diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index a6eb0e094..b05a70500 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -1,5 +1,6 @@ +from __future__ import annotations + import argparse -from typing import Optional from typing import Sequence import pre_commit.constants as C @@ -27,7 +28,7 @@ def check_all_hooks_match_files(config_file: str) -> int: return retv -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*', default=[C.CONFIG_FILE]) args = parser.parse_args(argv) diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 60870f835..0a8249b85 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -1,6 +1,7 @@ +from __future__ import annotations + import argparse import re -from typing import Optional from typing import Sequence from cfgv import apply_defaults @@ -65,7 +66,7 @@ def check_useless_excludes(config_file: str) -> int: return retv -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*', default=[C.CONFIG_FILE]) args = parser.parse_args(argv) diff --git a/pre_commit/meta_hooks/identity.py b/pre_commit/meta_hooks/identity.py index 12eb02f92..72ee440bc 100644 --- a/pre_commit/meta_hooks/identity.py +++ b/pre_commit/meta_hooks/identity.py @@ -1,11 +1,12 @@ +from __future__ import annotations + import sys -from typing import Optional from typing import Sequence from pre_commit import output -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: argv = argv if argv is not None else sys.argv[1:] for arg in argv: output.write_line(arg) diff --git a/pre_commit/output.py b/pre_commit/output.py index 24f9d8465..4bcf27f94 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import contextlib import sys from typing import Any from typing import IO -from typing import Optional def write(s: str, stream: IO[bytes] = sys.stdout.buffer) -> None: @@ -11,9 +12,9 @@ def write(s: str, stream: IO[bytes] = sys.stdout.buffer) -> None: def write_line_b( - s: Optional[bytes] = None, + s: bytes | None = None, stream: IO[bytes] = sys.stdout.buffer, - logfile_name: Optional[str] = None, + logfile_name: str | None = None, ) -> None: with contextlib.ExitStack() as exit_stack: output_streams = [stream] @@ -28,5 +29,5 @@ def write_line_b( output_stream.flush() -def write_line(s: Optional[str] = None, **kwargs: Any) -> None: +def write_line(s: str | None = None, **kwargs: Any) -> None: write_line_b(s.encode() if s is not None else s, **kwargs) diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index d344a1dab..3fd3129f1 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -1,7 +1,7 @@ +from __future__ import annotations + import os.path from typing import Mapping -from typing import Optional -from typing import Tuple from typing import TYPE_CHECKING from identify.identify import parse_shebang_from_file @@ -11,11 +11,11 @@ class ExecutableNotFoundError(OSError): - def to_output(self) -> Tuple[int, bytes, None]: + def to_output(self) -> tuple[int, bytes, None]: return (1, self.args[0].encode(), None) -def parse_filename(filename: str) -> Tuple[str, ...]: +def parse_filename(filename: str) -> tuple[str, ...]: if not os.path.exists(filename): return () else: @@ -23,8 +23,8 @@ def parse_filename(filename: str) -> Tuple[str, ...]: def find_executable( - exe: str, _environ: Optional[Mapping[str, str]] = None, -) -> Optional[str]: + exe: str, _environ: Mapping[str, str] | None = None, +) -> str | None: exe = os.path.normpath(exe) if os.sep in exe: return exe @@ -47,7 +47,7 @@ def find_executable( def normexe(orig: str) -> str: - def _error(msg: str) -> 'NoReturn': + def _error(msg: str) -> NoReturn: raise ExecutableNotFoundError(f'Executable `{orig}` {msg}') if os.sep not in orig and (not os.altsep or os.altsep not in orig): @@ -65,7 +65,7 @@ def _error(msg: str) -> 'NoReturn': return orig -def normalize_cmd(cmd: Tuple[str, ...]) -> Tuple[str, ...]: +def normalize_cmd(cmd: tuple[str, ...]) -> tuple[str, ...]: """Fixes for the following issues on windows - https://bugs.python.org/issue8557 - windows does not parse shebangs diff --git a/pre_commit/prefix.py b/pre_commit/prefix.py index 0e3ebbd89..f1b28c1d6 100644 --- a/pre_commit/prefix.py +++ b/pre_commit/prefix.py @@ -1,6 +1,7 @@ +from __future__ import annotations + import os.path from typing import NamedTuple -from typing import Tuple class Prefix(NamedTuple): @@ -12,6 +13,6 @@ def path(self, *parts: str) -> str: def exists(self, *parts: str) -> bool: return os.path.exists(self.path(*parts)) - def star(self, end: str) -> Tuple[str, ...]: + def star(self, end: str) -> tuple[str, ...]: paths = os.listdir(self.prefix_dir) return tuple(path for path in paths if path.endswith(end)) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 15827dde4..ac5d294bd 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -1,13 +1,10 @@ +from __future__ import annotations + import json import logging import os from typing import Any -from typing import Dict -from typing import List -from typing import Optional from typing import Sequence -from typing import Set -from typing import Tuple import pre_commit.constants as C from pre_commit.clientlib import load_manifest @@ -33,7 +30,7 @@ def _state_filename(prefix: Prefix, venv: str) -> str: return prefix.path(venv, f'.install_state_v{C.INSTALLED_STATE_VERSION}') -def _read_state(prefix: Prefix, venv: str) -> Optional[object]: +def _read_state(prefix: Prefix, venv: str) -> object | None: filename = _state_filename(prefix, venv) if not os.path.exists(filename): return None @@ -93,9 +90,9 @@ def _hook_install(hook: Hook) -> None: def _hook( - *hook_dicts: Dict[str, Any], - root_config: Dict[str, Any], -) -> Dict[str, Any]: + *hook_dicts: dict[str, Any], + root_config: dict[str, Any], +) -> dict[str, Any]: ret, rest = dict(hook_dicts[0]), hook_dicts[1:] for dct in rest: ret.update(dct) @@ -140,10 +137,10 @@ def _hook( def _non_cloned_repository_hooks( - repo_config: Dict[str, Any], + repo_config: dict[str, Any], store: Store, - root_config: Dict[str, Any], -) -> Tuple[Hook, ...]: + root_config: dict[str, Any], +) -> tuple[Hook, ...]: def _prefix(language_name: str, deps: Sequence[str]) -> Prefix: language = languages[language_name] # pygrep / script / system / docker_image do not have @@ -164,10 +161,10 @@ def _prefix(language_name: str, deps: Sequence[str]) -> Prefix: def _cloned_repository_hooks( - repo_config: Dict[str, Any], + repo_config: dict[str, Any], store: Store, - root_config: Dict[str, Any], -) -> Tuple[Hook, ...]: + root_config: dict[str, Any], +) -> tuple[Hook, ...]: repo, rev = repo_config['repo'], repo_config['rev'] manifest_path = os.path.join(store.clone(repo, rev), C.MANIFEST_FILE) by_id = {hook['id']: hook for hook in load_manifest(manifest_path)} @@ -196,10 +193,10 @@ def _cloned_repository_hooks( def _repository_hooks( - repo_config: Dict[str, Any], + repo_config: dict[str, Any], store: Store, - root_config: Dict[str, Any], -) -> Tuple[Hook, ...]: + root_config: dict[str, Any], +) -> tuple[Hook, ...]: if repo_config['repo'] in {LOCAL, META}: return _non_cloned_repository_hooks(repo_config, store, root_config) else: @@ -207,8 +204,8 @@ def _repository_hooks( def install_hook_envs(hooks: Sequence[Hook], store: Store) -> None: - def _need_installed() -> List[Hook]: - seen: Set[Tuple[Prefix, str, str, Tuple[str, ...]]] = set() + def _need_installed() -> list[Hook]: + seen: set[tuple[Prefix, str, str, tuple[str, ...]]] = set() ret = [] for hook in hooks: if hook.install_key not in seen and not _hook_installed(hook): @@ -224,7 +221,7 @@ def _need_installed() -> List[Hook]: _hook_install(hook) -def all_hooks(root_config: Dict[str, Any], store: Store) -> Tuple[Hook, ...]: +def all_hooks(root_config: dict[str, Any], store: Store) -> tuple[Hook, ...]: return tuple( hook for repo in root_config['repos'] diff --git a/pre_commit/resources/empty_template_setup.py b/pre_commit/resources/empty_template_setup.py index ef05eef84..870d0fbae 100644 --- a/pre_commit/resources/empty_template_setup.py +++ b/pre_commit/resources/empty_template_setup.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from setuptools import setup diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index bad004cd1..7e75080d4 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import logging import os.path diff --git a/pre_commit/store.py b/pre_commit/store.py index 27d8553c8..effebfb88 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import logging import os.path @@ -5,10 +7,7 @@ import tempfile from typing import Callable from typing import Generator -from typing import List -from typing import Optional from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit import file_lock @@ -40,7 +39,7 @@ def _get_default_directory() -> str: class Store: get_default_directory = staticmethod(_get_default_directory) - def __init__(self, directory: Optional[str] = None) -> None: + def __init__(self, directory: str | None = None) -> None: self.directory = directory or Store.get_default_directory() self.db_path = os.path.join(self.directory, 'db.db') self.readonly = ( @@ -92,7 +91,7 @@ def blocked_cb() -> None: # pragma: no cover (tests are in-process) @contextlib.contextmanager def connect( self, - db_path: Optional[str] = None, + db_path: str | None = None, ) -> Generator[sqlite3.Connection, None, None]: db_path = db_path or self.db_path # sqlite doesn't close its fd with its contextmanager >.< @@ -119,7 +118,7 @@ def _new_repo( ) -> str: repo = self.db_repo_name(repo, deps) - def _get_result() -> Optional[str]: + def _get_result() -> str | None: # Check if we already exist with self.connect() as db: result = db.execute( @@ -239,18 +238,18 @@ def mark_config_used(self, path: str) -> None: self._create_config_table(db) db.execute('INSERT OR IGNORE INTO configs VALUES (?)', (path,)) - def select_all_configs(self) -> List[str]: + def select_all_configs(self) -> list[str]: with self.connect() as db: self._create_config_table(db) rows = db.execute('SELECT path FROM configs').fetchall() return [path for path, in rows] - def delete_configs(self, configs: List[str]) -> None: + def delete_configs(self, configs: list[str]) -> None: with self.connect() as db: rows = [(path,) for path in configs] db.executemany('DELETE FROM configs WHERE path = ?', rows) - def select_all_repos(self) -> List[Tuple[str, str, str]]: + def select_all_repos(self) -> list[tuple[str, str, str]]: with self.connect() as db: return db.execute('SELECT repo, ref, path from repos').fetchall() diff --git a/pre_commit/util.py b/pre_commit/util.py index 6977acb2c..40c53e515 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -1,6 +1,9 @@ +from __future__ import annotations + import contextlib import errno import functools +import importlib.resources import os.path import shutil import stat @@ -10,24 +13,13 @@ from types import TracebackType from typing import Any from typing import Callable -from typing import Dict from typing import Generator from typing import IO -from typing import Optional -from typing import Tuple -from typing import Type import yaml from pre_commit import parse_shebang -if sys.version_info >= (3, 7): # pragma: >=3.7 cover - from importlib.resources import open_binary - from importlib.resources import read_text -else: # pragma: <3.7 cover - from importlib_resources import open_binary - from importlib_resources import read_text - Loader = getattr(yaml, 'CSafeLoader', yaml.SafeLoader) yaml_load = functools.partial(yaml.load, Loader=Loader) Dumper = getattr(yaml, 'CSafeDumper', yaml.SafeDumper) @@ -73,11 +65,11 @@ def tmpdir() -> Generator[str, None, None]: def resource_bytesio(filename: str) -> IO[bytes]: - return open_binary('pre_commit.resources', filename) + return importlib.resources.open_binary('pre_commit.resources', filename) def resource_text(filename: str) -> str: - return read_text('pre_commit.resources', filename) + return importlib.resources.read_text('pre_commit.resources', filename) def make_executable(filename: str) -> None: @@ -90,10 +82,10 @@ class CalledProcessError(RuntimeError): def __init__( self, returncode: int, - cmd: Tuple[str, ...], + cmd: tuple[str, ...], expected_returncode: int, stdout: bytes, - stderr: Optional[bytes], + stderr: bytes | None, ) -> None: super().__init__(returncode, cmd, expected_returncode, stdout, stderr) self.returncode = returncode @@ -103,7 +95,7 @@ def __init__( self.stderr = stderr def __bytes__(self) -> bytes: - def _indent_or_none(part: Optional[bytes]) -> bytes: + def _indent_or_none(part: bytes | None) -> bytes: if part: return b'\n ' + part.replace(b'\n', b'\n ') else: @@ -121,20 +113,20 @@ def __str__(self) -> str: return self.__bytes__().decode() -def _setdefault_kwargs(kwargs: Dict[str, Any]) -> None: +def _setdefault_kwargs(kwargs: dict[str, Any]) -> None: for arg in ('stdin', 'stdout', 'stderr'): kwargs.setdefault(arg, subprocess.PIPE) -def _oserror_to_output(e: OSError) -> Tuple[int, bytes, None]: +def _oserror_to_output(e: OSError) -> tuple[int, bytes, None]: return 1, force_bytes(e).rstrip(b'\n') + b'\n', None def cmd_output_b( *cmd: str, - retcode: Optional[int] = 0, + retcode: int | None = 0, **kwargs: Any, -) -> Tuple[int, bytes, Optional[bytes]]: +) -> tuple[int, bytes, bytes | None]: _setdefault_kwargs(kwargs) try: @@ -156,7 +148,7 @@ def cmd_output_b( return returncode, stdout_b, stderr_b -def cmd_output(*cmd: str, **kwargs: Any) -> Tuple[int, str, Optional[str]]: +def cmd_output(*cmd: str, **kwargs: Any) -> tuple[int, str, str | None]: returncode, stdout_b, stderr_b = cmd_output_b(*cmd, **kwargs) stdout = stdout_b.decode() if stdout_b is not None else None stderr = stderr_b.decode() if stderr_b is not None else None @@ -169,10 +161,10 @@ def cmd_output(*cmd: str, **kwargs: Any) -> Tuple[int, str, Optional[str]]: class Pty: def __init__(self) -> None: - self.r: Optional[int] = None - self.w: Optional[int] = None + self.r: int | None = None + self.w: int | None = None - def __enter__(self) -> 'Pty': + def __enter__(self) -> Pty: self.r, self.w = openpty() # tty flags normally change \n to \r\n @@ -195,18 +187,18 @@ def close_r(self) -> None: def __exit__( self, - exc_type: Optional[Type[BaseException]], - exc_value: Optional[BaseException], - traceback: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, ) -> None: self.close_w() self.close_r() def cmd_output_p( *cmd: str, - retcode: Optional[int] = 0, + retcode: int | None = 0, **kwargs: Any, - ) -> Tuple[int, bytes, Optional[bytes]]: + ) -> tuple[int, bytes, bytes | None]: assert retcode is None assert kwargs['stderr'] == subprocess.STDOUT, kwargs['stderr'] _setdefault_kwargs(kwargs) @@ -250,7 +242,7 @@ def rmtree(path: str) -> None: def handle_remove_readonly( func: Callable[..., Any], path: str, - exc: Tuple[Type[OSError], OSError, TracebackType], + exc: tuple[type[OSError], OSError, TracebackType], ) -> None: excvalue = exc[1] if ( @@ -265,7 +257,7 @@ def handle_remove_readonly( shutil.rmtree(path, ignore_errors=False, onerror=handle_remove_readonly) -def parse_version(s: str) -> Tuple[int, ...]: +def parse_version(s: str) -> tuple[int, ...]: """poor man's version comparison""" return tuple(int(p) for p in s.split('.')) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 6b0fa2086..f2b3421a0 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import concurrent.futures import contextlib import math @@ -8,11 +10,8 @@ from typing import Callable from typing import Generator from typing import Iterable -from typing import List from typing import MutableMapping -from typing import Optional from typing import Sequence -from typing import Tuple from typing import TypeVar from pre_commit import parse_shebang @@ -23,7 +22,7 @@ TRet = TypeVar('TRet') -def _environ_size(_env: Optional[MutableMapping[str, str]] = None) -> int: +def _environ_size(_env: MutableMapping[str, str] | None = None) -> int: environ = _env if _env is not None else getattr(os, 'environb', os.environ) size = 8 * len(environ) # number of pointers in `envp` for k, v in environ.items(): @@ -62,8 +61,8 @@ def partition( cmd: Sequence[str], varargs: Sequence[str], target_concurrency: int, - _max_length: Optional[int] = None, -) -> Tuple[Tuple[str, ...], ...]: + _max_length: int | None = None, +) -> tuple[tuple[str, ...], ...]: _max_length = _max_length or _get_platform_max_length() # Generally, we try to partition evenly into at least `target_concurrency` @@ -73,7 +72,7 @@ def partition( cmd = tuple(cmd) ret = [] - ret_cmd: List[str] = [] + ret_cmd: list[str] = [] # Reversed so arguments are in order varargs = list(reversed(varargs)) @@ -115,14 +114,14 @@ def _thread_mapper(maxsize: int) -> Generator[ def xargs( - cmd: Tuple[str, ...], + cmd: tuple[str, ...], varargs: Sequence[str], *, color: bool = False, target_concurrency: int = 1, _max_length: int = _get_platform_max_length(), **kwargs: Any, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: """A simplified implementation of xargs. color: Make a pty if on a platform that supports it @@ -152,8 +151,8 @@ def xargs( partitions = partition(cmd, varargs, target_concurrency, _max_length) def run_cmd_partition( - run_cmd: Tuple[str, ...], - ) -> Tuple[int, bytes, Optional[bytes]]: + run_cmd: tuple[str, ...], + ) -> tuple[int, bytes, bytes | None]: return cmd_fn( *run_cmd, retcode=None, stderr=subprocess.STDOUT, **kwargs, ) diff --git a/setup.cfg b/setup.cfg index ef55b7cdc..d712a3f30 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,7 +13,6 @@ classifiers = License :: OSI Approved :: MIT License Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 @@ -31,8 +30,7 @@ install_requires = toml virtualenv>=20.0.8 importlib-metadata;python_version<"3.8" - importlib-resources<5.3;python_version<"3.7" -python_requires = >=3.6.1 +python_requires = >=3.7 [options.packages.find] exclude = diff --git a/setup.py b/setup.py index 8bf1ba938..3d93aefb2 100644 --- a/setup.py +++ b/setup.py @@ -1,2 +1,4 @@ +from __future__ import annotations + from setuptools import setup setup() diff --git a/testing/auto_namedtuple.py b/testing/auto_namedtuple.py index 0841094eb..d5a43775b 100644 --- a/testing/auto_namedtuple.py +++ b/testing/auto_namedtuple.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import collections diff --git a/testing/fixtures.py b/testing/fixtures.py index f7def081f..ef5a04185 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import os.path import shutil diff --git a/testing/gen-languages-all b/testing/gen-languages-all index 152cf3c6f..dfd92c0ed 100755 --- a/testing/gen-languages-all +++ b/testing/gen-languages-all @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +from __future__ import annotations + import sys LANGUAGES = [ diff --git a/testing/make-archives b/testing/make-archives index ce098ba1f..ed95fb7cd 100755 --- a/testing/make-archives +++ b/testing/make-archives @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +from __future__ import annotations + import argparse import gzip import os.path @@ -6,7 +8,6 @@ import shutil import subprocess import tarfile import tempfile -from typing import Optional from typing import Sequence @@ -69,7 +70,7 @@ def make_archive(name: str, repo: str, ref: str, destdir: str) -> str: return output_path -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('--dest', default='pre_commit/resources') args = parser.parse_args(argv) diff --git a/testing/resources/python_hooks_repo/foo.py b/testing/resources/python_hooks_repo/foo.py index 9c4368e20..40efde392 100644 --- a/testing/resources/python_hooks_repo/foo.py +++ b/testing/resources/python_hooks_repo/foo.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys diff --git a/testing/resources/python_hooks_repo/setup.py b/testing/resources/python_hooks_repo/setup.py index 0559271ee..cff6cadf3 100644 --- a/testing/resources/python_hooks_repo/setup.py +++ b/testing/resources/python_hooks_repo/setup.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from setuptools import setup setup( diff --git a/testing/resources/python_venv_hooks_repo/foo.py b/testing/resources/python_venv_hooks_repo/foo.py index 9c4368e20..40efde392 100644 --- a/testing/resources/python_venv_hooks_repo/foo.py +++ b/testing/resources/python_venv_hooks_repo/foo.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys diff --git a/testing/resources/python_venv_hooks_repo/setup.py b/testing/resources/python_venv_hooks_repo/setup.py index 0559271ee..cff6cadf3 100644 --- a/testing/resources/python_venv_hooks_repo/setup.py +++ b/testing/resources/python_venv_hooks_repo/setup.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from setuptools import setup setup( diff --git a/testing/util.py b/testing/util.py index 283ed4776..0dd178406 100644 --- a/testing/util.py +++ b/testing/util.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import os.path import subprocess diff --git a/testing/zipapp/Dockerfile b/testing/zipapp/Dockerfile index e21d5fe31..7c74c1b2e 100644 --- a/testing/zipapp/Dockerfile +++ b/testing/zipapp/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:bionic +FROM ubuntu:focal RUN : \ && apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ @@ -10,5 +10,5 @@ RUN : \ ENV LANG=C.UTF-8 PATH=/venv/bin:$PATH RUN : \ - && python3.6 -mvenv /venv \ + && python3 -mvenv /venv \ && pip install --no-cache-dir pip setuptools wheel no-manylinux --upgrade diff --git a/testing/zipapp/entry b/testing/zipapp/entry index 87f9291dc..15758d936 100755 --- a/testing/zipapp/entry +++ b/testing/zipapp/entry @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +from __future__ import annotations + import os.path import shutil import stat @@ -59,10 +61,7 @@ def main() -> int: if sys.platform == 'win32': # https://bugs.python.org/issue19124 import subprocess - if sys.version_info < (3, 7): # https://bugs.python.org/issue25942 - return subprocess.Popen(cmd).wait() - else: - return subprocess.call(cmd) + return subprocess.call(cmd) else: os.execvp(cmd[0], cmd) diff --git a/testing/zipapp/make b/testing/zipapp/make index effc81234..37b5c355d 100755 --- a/testing/zipapp/make +++ b/testing/zipapp/make @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +from __future__ import annotations + import argparse import base64 import hashlib diff --git a/testing/zipapp/python b/testing/zipapp/python index 7184a1aae..67910fca8 100755 --- a/testing/zipapp/python +++ b/testing/zipapp/python @@ -1,5 +1,7 @@ #!/usr/bin/env python3 """A shim executable to put dependencies on sys.path""" +from __future__ import annotations + import argparse import os.path import runpy @@ -36,10 +38,7 @@ def main() -> int: if sys.platform == 'win32': # https://bugs.python.org/issue19124 import subprocess - if sys.version_info < (3, 7): # https://bugs.python.org/issue25942 - return subprocess.Popen(cmd).wait() - else: - return subprocess.call(cmd) + return subprocess.call(cmd) else: os.execvp(cmd[0], cmd) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 39a371689..3fb3af523 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging import re diff --git a/tests/color_test.py b/tests/color_test.py index 5cd226a9e..89b4fd3eb 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys from unittest import mock diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 7316eb97a..3a142661a 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import shlex from unittest import mock diff --git a/tests/commands/clean_test.py b/tests/commands/clean_test.py index 955a6bc4e..dd8e4a53a 100644 --- a/tests/commands/clean_test.py +++ b/tests/commands/clean_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path from unittest import mock diff --git a/tests/commands/gc_test.py b/tests/commands/gc_test.py index 02b36945b..c128e9393 100644 --- a/tests/commands/gc_test.py +++ b/tests/commands/gc_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import pre_commit.constants as C diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index 37b78bc0d..b0159f8e3 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import subprocess import sys from unittest import mock diff --git a/tests/commands/init_templatedir_test.py b/tests/commands/init_templatedir_test.py index 4e131dff7..64bfc8b4e 100644 --- a/tests/commands/init_templatedir_test.py +++ b/tests/commands/init_templatedir_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path from unittest import mock diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 0b2e248b1..703ba03b7 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import re diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index f5eddd3d6..b80244e12 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pre_commit.constants as C from pre_commit.commands.migrate_config import migrate_config diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 3a6fa2a10..085b063f3 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import shlex import sys diff --git a/tests/commands/sample_config_test.py b/tests/commands/sample_config_test.py index 8e3a9043f..cf56e98c4 100644 --- a/tests/commands/sample_config_test.py +++ b/tests/commands/sample_config_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pre_commit.commands.sample_config import sample_config diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index a157d1636..0b2db7e5a 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import re import time diff --git a/tests/conftest.py b/tests/conftest.py index f38f9693c..b68a1d00c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import functools import io import logging diff --git a/tests/envcontext_test.py b/tests/envcontext_test.py index f9d4dce69..c82d3267d 100644 --- a/tests/envcontext_test.py +++ b/tests/envcontext_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os from unittest import mock diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index cb76dcf47..31c71d287 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import stat import sys diff --git a/tests/git_test.py b/tests/git_test.py index bcb3fd15b..d9e497c5b 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import pytest diff --git a/tests/languages/conda_test.py b/tests/languages/conda_test.py index 6faa78f21..5023b2afb 100644 --- a/tests/languages/conda_test.py +++ b/tests/languages/conda_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from pre_commit import envcontext diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index ec6bb83ca..58387611f 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import builtins import json import ntpath diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py index 9a64ed195..9e393cb39 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from pre_commit.languages.golang import guess_go_dir diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index fd9b9a459..49d81226e 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import multiprocessing import os.path import sys diff --git a/tests/languages/node_test.py b/tests/languages/node_test.py index 8e52268ff..fb5ae7172 100644 --- a/tests/languages/node_test.py +++ b/tests/languages/node_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json import os import shutil diff --git a/tests/languages/pygrep_test.py b/tests/languages/pygrep_test.py index d8bacc484..8420046c5 100644 --- a/tests/languages/pygrep_test.py +++ b/tests/languages/pygrep_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from pre_commit.languages import pygrep diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 8324cac20..616066965 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import sys from unittest import mock @@ -47,16 +49,16 @@ def test_norm_version_of_default_is_sys_executable(): assert python.norm_version('default') is None -@pytest.mark.parametrize('v', ('python3.6', 'python3', 'python')) +@pytest.mark.parametrize('v', ('python3.9', 'python3', 'python')) def test_sys_executable_matches(v): - with mock.patch.object(sys, 'version_info', (3, 6, 7)): + with mock.patch.object(sys, 'version_info', (3, 9, 10)): assert python._sys_executable_matches(v) assert python.norm_version(v) is None @pytest.mark.parametrize('v', ('notpython', 'python3.x')) def test_sys_executable_matches_does_not_match(v): - with mock.patch.object(sys, 'version_info', (3, 6, 7)): + with mock.patch.object(sys, 'version_info', (3, 9, 10)): assert not python._sys_executable_matches(v) @@ -65,7 +67,7 @@ def test_sys_executable_matches_does_not_match(v): ('/usr/bin/python3', '/usr/bin/python3.7', 'python3'), ('/usr/bin/python', '/usr/bin/python3.7', 'python3.7'), ('/usr/bin/python', '/usr/bin/python', None), - ('/usr/bin/python3.6m', '/usr/bin/python3.6m', 'python3.6m'), + ('/usr/bin/python3.7m', '/usr/bin/python3.7m', 'python3.7m'), ('v/bin/python', 'v/bin/pypy', 'pypy'), ), ) diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py index 66aa7b388..bc302a795 100644 --- a/tests/languages/r_test.py +++ b/tests/languages/r_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import pytest diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 7dff0466b..dc55456e8 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import tarfile from unittest import mock diff --git a/tests/logging_handler_test.py b/tests/logging_handler_test.py index fe68593b9..dc43a99f5 100644 --- a/tests/logging_handler_test.py +++ b/tests/logging_handler_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging from pre_commit import color diff --git a/tests/main_test.py b/tests/main_test.py index 1ad8d418e..64b26a00c 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import argparse import os.path from unittest import mock diff --git a/tests/meta_hooks/check_hooks_apply_test.py b/tests/meta_hooks/check_hooks_apply_test.py index 06bdd0455..63f971520 100644 --- a/tests/meta_hooks/check_hooks_apply_test.py +++ b/tests/meta_hooks/check_hooks_apply_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pre_commit.meta_hooks import check_hooks_apply from testing.fixtures import add_config_to_repo diff --git a/tests/meta_hooks/check_useless_excludes_test.py b/tests/meta_hooks/check_useless_excludes_test.py index 703bd250c..15b68b4cb 100644 --- a/tests/meta_hooks/check_useless_excludes_test.py +++ b/tests/meta_hooks/check_useless_excludes_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pre_commit import git from pre_commit.meta_hooks import check_useless_excludes from pre_commit.util import cmd_output diff --git a/tests/meta_hooks/identity_test.py b/tests/meta_hooks/identity_test.py index 3eff00be3..97c20ea64 100644 --- a/tests/meta_hooks/identity_test.py +++ b/tests/meta_hooks/identity_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pre_commit.meta_hooks import identity diff --git a/tests/output_test.py b/tests/output_test.py index 1cdacbbce..c806829aa 100644 --- a/tests/output_test.py +++ b/tests/output_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import io from pre_commit import output diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index 0bb19c78b..d7acbf577 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import os.path import shutil diff --git a/tests/prefix_test.py b/tests/prefix_test.py index 6ce8be127..1eac087df 100644 --- a/tests/prefix_test.py +++ b/tests/prefix_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import pytest diff --git a/tests/repository_test.py b/tests/repository_test.py index 8569ba96c..013731476 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import os.path import shutil import sys from typing import Any -from typing import Dict from unittest import mock import cfgv @@ -897,7 +898,7 @@ def test_local_python_repo(store, local_python_config): def test_default_language_version(store, local_python_config): - config: Dict[str, Any] = { + config: dict[str, Any] = { 'default_language_version': {'python': 'fake'}, 'default_stages': ['commit'], 'repos': [local_python_config], @@ -914,7 +915,7 @@ def test_default_language_version(store, local_python_config): def test_default_stages(store, local_python_config): - config: Dict[str, Any] = { + config: dict[str, Any] = { 'default_language_version': {'python': C.DEFAULT}, 'default_stages': ['commit'], 'repos': [local_python_config], diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 2e3f62091..a91f31519 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import itertools import os.path import shutil diff --git a/tests/store_test.py b/tests/store_test.py index 5a5d69e0f..ff671a83d 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import sqlite3 import stat diff --git a/tests/util_test.py b/tests/util_test.py index 01afbd4bf..6b00f9fc6 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import stat import subprocess diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 7e83ef590..0530e50d1 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import concurrent.futures import os import sys import time -from typing import Tuple from unittest import mock import pytest @@ -178,7 +179,7 @@ def test_thread_mapper_concurrency_uses_regular_map(): def test_xargs_propagate_kwargs_to_cmd(): env = {'PRE_COMMIT_TEST_VAR': 'Pre commit is awesome'} - cmd: Tuple[str, ...] = ('bash', '-c', 'echo $PRE_COMMIT_TEST_VAR', '--') + cmd: tuple[str, ...] = ('bash', '-c', 'echo $PRE_COMMIT_TEST_VAR', '--') cmd = parse_shebang.normalize_cmd(cmd) ret, stdout = xargs.xargs(cmd, ('1',), env=env) diff --git a/tox.ini b/tox.ini index 11b20d418..7f43e41e4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py36,py37,py38,pypy3,pre-commit +envlist = py37,py38,pypy3,pre-commit [testenv] deps = -rrequirements-dev.txt From 1112c9f5ce4f2b0497efb4fe91ce8ae0681e049d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 23 Jan 2022 21:20:59 -0500 Subject: [PATCH 601/967] upgrade flake8-typing-imports Committed via https://github.com/asottile/all-repos --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5103e0bef..ce2dd34f2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: rev: 4.0.1 hooks: - id: flake8 - additional_dependencies: [flake8-typing-imports==1.10.0] + additional_dependencies: [flake8-typing-imports==1.12.0] - repo: https://github.com/pre-commit/mirrors-autopep8 rev: v1.6.0 hooks: From 8e9202acb41d4407b0af2766ee612576a4dcbcc3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 31 Jan 2022 21:02:21 +0000 Subject: [PATCH 602/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder_python_imports: v2.6.0 → v2.7.1](https://github.com/asottile/reorder_python_imports/compare/v2.6.0...v2.7.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ce2dd34f2..b0de20968 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/asottile/reorder_python_imports - rev: v2.6.0 + rev: v2.7.1 hooks: - id: reorder-python-imports args: [--py37-plus, --add-import, 'from __future__ import annotations'] From e58bcb51fc40c1006c7d932b46eb19ffaad14e29 Mon Sep 17 00:00:00 2001 From: Lee Trout Date: Wed, 2 Mar 2022 17:33:11 -0500 Subject: [PATCH 603/967] Fix typo in help docs for to-ref and from-ref --- pre_commit/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 7ab9515c7..da96a0119 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -106,7 +106,7 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: parser.add_argument( '--from-ref', '--source', '-s', help=( - '(for usage with `--from-ref`) -- this option represents the ' + '(for usage with `--to-ref`) -- this option represents the ' 'original ref in a `from_ref...to_ref` diff expression. ' 'For `pre-push` hooks, this represents the branch you are pushing ' 'to. ' @@ -117,7 +117,7 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: parser.add_argument( '--to-ref', '--origin', '-o', help=( - '(for usage with `--to-ref`) -- this option represents the ' + '(for usage with `--from-ref`) -- this option represents the ' 'destination ref in a `from_ref...to_ref` diff expression. ' 'For `pre-push` hooks, this represents the branch being pushed. ' 'For `post-checkout` hooks, this represents the branch that is ' From 07f441584b9e4111e31e857a6ee94fcadfb704c4 Mon Sep 17 00:00:00 2001 From: VincentBerthier <34085617+VincentBerthier@users.noreply.github.com> Date: Fri, 4 Mar 2022 20:18:27 +0100 Subject: [PATCH 604/967] GIT_HTTP_PROXY_AUTHMETHOD kept in env variables --- pre_commit/git.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pre_commit/git.py b/pre_commit/git.py index 67499cdb8..853f4b0d0 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -43,6 +43,7 @@ def no_git_env( k in { 'GIT_EXEC_PATH', 'GIT_SSH', 'GIT_SSH_COMMAND', 'GIT_SSL_CAINFO', 'GIT_SSL_NO_VERIFY', 'GIT_CONFIG_COUNT', + 'GIT_HTTP_PROXY_AUTHMETHOD', } } From 65755af7e3890f7ee130c0c1fdaa0429a868030e Mon Sep 17 00:00:00 2001 From: Lorenz Walthert Date: Sat, 5 Mar 2022 21:04:01 +0100 Subject: [PATCH 605/967] inline options() to always install binaries --- pre_commit/languages/r.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 2ad8c411b..9b32b2d74 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -103,9 +103,7 @@ def install_environment( shutil.copy(prefix.path('renv.lock'), env_dir) shutil.copytree(prefix.path('renv'), os.path.join(env_dir, 'renv')) - cmd_output_b( - _rscript_exec(), '--vanilla', '-e', - f"""\ + r_code_inst_environment = f"""\ prefix_dir <- {prefix.prefix_dir!r} options( repos = c(CRAN = "https://cran.rstudio.com"), @@ -132,19 +130,36 @@ def install_environment( if (is_package) {{ renv::install(prefix_dir) }} - """, + """ + + cmd_output_b( + _rscript_exec(), '--vanilla', '-e', + _inline_r_setup(r_code_inst_environment), cwd=env_dir, ) if additional_dependencies: + r_code_inst_add = 'renv::install(commandArgs(trailingOnly = TRUE))' with in_env(prefix, version): cmd_output_b( _rscript_exec(), *RSCRIPT_OPTS, '-e', - 'renv::install(commandArgs(trailingOnly = TRUE))', + _inline_r_setup(r_code_inst_add), *additional_dependencies, cwd=env_dir, ) +def _inline_r_setup(code: str) -> str: + """ + Some behaviour of R cannot be configured via env variables, but can + only be configured via R options once R has started. These are set here. + """ + with_option = f"""\ + options(install.packages.compile.from.source = "never") + {code} + """ + return with_option + + def run_hook( hook: Hook, file_args: Sequence[str], From a85df8027b09bda436b4bba5154ae3bc474f6ceb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 13 Mar 2022 19:55:30 -0400 Subject: [PATCH 606/967] remove unneeded gitignore lines - coverage-html: coverage>=6.2 writes a .gitignore file - mypy_cache: mypy>=0.770 writes a .gitignore file - pytest_cache: pytest>=3.8.1 writes a .gitignore file - venv: virtualenv>=20.0.21 writes a .gitignore file Committed via https://github.com/asottile/all-repos --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 4f4f6b941..c2021816c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,6 @@ *.egg-info *.py[co] /.coverage -/.mypy_cache -/.pytest_cache /.tox /dist -/venv* .vscode/ From 9516ed41aa89904e9771b59c748cb3be90689d52 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Mar 2022 22:18:01 +0000 Subject: [PATCH 607/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.31.0 → v2.31.1](https://github.com/asottile/pyupgrade/compare/v2.31.0...v2.31.1) - [github.com/asottile/reorder_python_imports: v2.7.1 → v3.0.1](https://github.com/asottile/reorder_python_imports/compare/v2.7.1...v3.0.1) - [github.com/pre-commit/mirrors-mypy: v0.931 → v0.940](https://github.com/pre-commit/mirrors-mypy/compare/v0.931...v0.940) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b0de20968..bea2466b6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,12 +25,12 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.31.0 + rev: v2.31.1 hooks: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/asottile/reorder_python_imports - rev: v2.7.1 + rev: v3.0.1 hooks: - id: reorder-python-imports args: [--py37-plus, --add-import, 'from __future__ import annotations'] @@ -45,7 +45,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.931 + rev: v0.940 hooks: - id: mypy additional_dependencies: [types-all] From a8225a250b4e567f6881362db0304ca574b251a4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 14 Mar 2022 18:37:07 -0400 Subject: [PATCH 608/967] convince mypy that these are the same --- pre_commit/error_handler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index a6a7329e7..992f5cdc0 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -6,6 +6,7 @@ import sys import traceback from typing import Generator +from typing import IO import pre_commit.constants as C from pre_commit import output @@ -32,7 +33,7 @@ def _log_and_exit( with contextlib.ExitStack() as ctx: if os.access(storedir, os.W_OK): output.write_line(f'Check the log at {log_path}') - log = ctx.enter_context(open(log_path, 'wb')) + log: IO[bytes] = ctx.enter_context(open(log_path, 'wb')) else: # pragma: win32 no cover output.write_line(f'Failed to write to log at {log_path}') log = sys.stdout.buffer From 678ef6b9fd4f84e10486dc592e0939549591f919 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Thu, 17 Mar 2022 15:31:01 +0100 Subject: [PATCH 609/967] coursier: Add support for both `cs` and `coursier` executable names On some systems, the executable might be named `coursier` instead of `cs`. For example, this is the case on Arch Linux when using the AUR package, or when following the official instructions when installing the JAR-based launcher: https://get-coursier.io/docs/cli-installation#jar-based-launcher --- pre_commit/languages/coursier.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index e47f9c873..bb3e0b848 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -10,6 +10,7 @@ from pre_commit.envcontext import Var from pre_commit.hook import Hook from pre_commit.languages import helpers +from pre_commit.parse_shebang import find_executable from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure @@ -27,6 +28,14 @@ def install_environment( helpers.assert_version_default('coursier', version) helpers.assert_no_additional_deps('coursier', additional_dependencies) + # Support both possible executable names (either "cs" or "coursier") + executable = find_executable('cs') or find_executable('coursier') + if executable is None: + raise AssertionError( + 'pre-commit requires system-installed "cs" or "coursier" ' + 'executables in the application search path', + ) + envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) channel = prefix.path('.pre-commit-channel') with clean_path_on_failure(envdir): @@ -36,7 +45,7 @@ def install_environment( helpers.run_setup_cmd( prefix, ( - 'cs', + executable, 'install', '--default-channels=false', f'--channel={channel}', From 28a5a28b396120202a19535e28475dec3c9acd92 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 21 Mar 2022 22:31:22 +0000 Subject: [PATCH 610/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.940 → v0.941](https://github.com/pre-commit/mirrors-mypy/compare/v0.940...v0.941) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bea2466b6..a80505b0c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,7 +45,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.940 + rev: v0.941 hooks: - id: mypy additional_dependencies: [types-all] From 525191f34bcb26210ada2d90f76222920049f3ef Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 24 Mar 2022 13:52:25 -0400 Subject: [PATCH 611/967] update master to main --- CONTRIBUTING.md | 2 +- README.md | 6 +++--- azure-pipelines.yml | 2 +- pre_commit/main.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 76df43705..adce08f91 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -93,7 +93,7 @@ language, for example: here are the apis that should be implemented for a language -Note that these are also documented in [`pre_commit/languages/all.py`](https://github.com/pre-commit/pre-commit/blob/master/pre_commit/languages/all.py) +Note that these are also documented in [`pre_commit/languages/all.py`](https://github.com/pre-commit/pre-commit/blob/main/pre_commit/languages/all.py) #### `ENVIRONMENT_DIR` diff --git a/README.md b/README.md index de7032cb9..db1259c25 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -[![Build Status](https://dev.azure.com/asottile/asottile/_apis/build/status/pre-commit.pre-commit?branchName=master)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master) -[![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/21/master.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master) -[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/pre-commit/pre-commit/master.svg)](https://results.pre-commit.ci/latest/github/pre-commit/pre-commit/master) +[![Build Status](https://dev.azure.com/asottile/asottile/_apis/build/status/pre-commit.pre-commit?branchName=main)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=main) +[![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/21/main.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=main) +[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/pre-commit/pre-commit/main.svg)](https://results.pre-commit.ci/latest/github/pre-commit/pre-commit/main) ## pre-commit diff --git a/azure-pipelines.yml b/azure-pipelines.yml index d3336a46a..afb298289 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,6 +1,6 @@ trigger: branches: - include: [master, test-me-*] + include: [main, test-me-*] tags: include: ['*'] diff --git a/pre_commit/main.py b/pre_commit/main.py index da96a0119..f3c551885 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -197,7 +197,7 @@ def main(argv: Sequence[str] | None = None) -> int: autoupdate_parser.add_argument( '--bleeding-edge', action='store_true', help=( - 'Update to the bleeding edge of `master` instead of the latest ' + 'Update to the bleeding edge of `HEAD` instead of the latest ' 'tagged version (the default behavior).' ), ) From 97419b34defbddcaea6aeab04e77dd5af14a25f3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 25 Mar 2022 14:12:02 -0400 Subject: [PATCH 612/967] reorder pre-commit config Committed via https://github.com/asottile/all-repos --- .pre-commit-config.yaml | 47 +++++++------------ .../resources/python3_hooks_repo/py3_hook.py | 2 + testing/resources/python3_hooks_repo/setup.py | 2 + 3 files changed, 21 insertions(+), 30 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a80505b0c..6f1fafd20 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,53 +4,40 @@ repos: hooks: - id: trailing-whitespace - id: end-of-file-fixer - - id: check-docstring-first - - id: check-json - id: check-yaml - id: debug-statements + - id: double-quote-string-fixer - id: name-tests-test - id: requirements-txt-fixer - - id: double-quote-string-fixer -- repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 - hooks: - - id: flake8 - additional_dependencies: [flake8-typing-imports==1.12.0] -- repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.6.0 - hooks: - - id: autopep8 -- repo: https://github.com/pre-commit/pre-commit - rev: v2.17.0 - hooks: - - id: validate_manifest -- repo: https://github.com/asottile/pyupgrade - rev: v2.31.1 +- repo: https://github.com/asottile/setup-cfg-fmt + rev: v1.20.0 hooks: - - id: pyupgrade - args: [--py37-plus] + - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder_python_imports rev: v3.0.1 hooks: - id: reorder-python-imports args: [--py37-plus, --add-import, 'from __future__ import annotations'] - exclude: ^testing/resources/python3_hooks_repo/ - repo: https://github.com/asottile/add-trailing-comma rev: v2.2.1 hooks: - id: add-trailing-comma args: [--py36-plus] -- repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.20.0 +- repo: https://github.com/asottile/pyupgrade + rev: v2.31.1 hooks: - - id: setup-cfg-fmt + - id: pyupgrade + args: [--py37-plus] +- repo: https://github.com/pre-commit/mirrors-autopep8 + rev: v1.6.0 + hooks: + - id: autopep8 +- repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 + hooks: + - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.941 + rev: v0.942 hooks: - id: mypy additional_dependencies: [types-all] - exclude: ^testing/resources/ -- repo: meta - hooks: - - id: check-hooks-apply - - id: check-useless-excludes diff --git a/testing/resources/python3_hooks_repo/py3_hook.py b/testing/resources/python3_hooks_repo/py3_hook.py index 8c9cda4c6..fd64ce8dd 100644 --- a/testing/resources/python3_hooks_repo/py3_hook.py +++ b/testing/resources/python3_hooks_repo/py3_hook.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys diff --git a/testing/resources/python3_hooks_repo/setup.py b/testing/resources/python3_hooks_repo/setup.py index 9125dc1df..49e1e2cc7 100644 --- a/testing/resources/python3_hooks_repo/setup.py +++ b/testing/resources/python3_hooks_repo/setup.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from setuptools import setup setup( From 3b9804062334d69582ff04299d22a8d2df60460a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 25 Mar 2022 14:31:33 -0400 Subject: [PATCH 613/967] fix pre-commit issues --- .pre-commit-config.yaml | 2 ++ testing/resources/python3_hooks_repo/py3_hook.py | 2 -- testing/resources/python3_hooks_repo/setup.py | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6f1fafd20..5525d71d7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,6 +17,7 @@ repos: rev: v3.0.1 hooks: - id: reorder-python-imports + exclude: ^testing/resources/python3_hooks_repo/ args: [--py37-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma rev: v2.2.1 @@ -41,3 +42,4 @@ repos: hooks: - id: mypy additional_dependencies: [types-all] + exclude: ^testing/resources/ diff --git a/testing/resources/python3_hooks_repo/py3_hook.py b/testing/resources/python3_hooks_repo/py3_hook.py index fd64ce8dd..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 annotations - import sys diff --git a/testing/resources/python3_hooks_repo/setup.py b/testing/resources/python3_hooks_repo/setup.py index 49e1e2cc7..9125dc1df 100644 --- a/testing/resources/python3_hooks_repo/setup.py +++ b/testing/resources/python3_hooks_repo/setup.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from setuptools import setup setup( From 2188c0fd2c4feefefe6ab7b2b45e8b4c4fa93acc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 30 Mar 2022 10:38:05 -0400 Subject: [PATCH 614/967] include the configured value in the language_version / additional_dependencies error --- pre_commit/languages/helpers.py | 8 +++++--- tests/languages/helpers_test.py | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index dd219ffa1..808082665 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -67,7 +67,8 @@ def environment_dir(d: str | None, language_version: str) -> str | None: def assert_version_default(binary: str, version: str) -> None: if version != C.DEFAULT: raise AssertionError( - f'For now, pre-commit requires system-installed {binary}', + f'for now, pre-commit requires system-installed {binary} -- ' + f'you selected `language_version: {version}`', ) @@ -77,8 +78,9 @@ def assert_no_additional_deps( ) -> None: if additional_deps: raise AssertionError( - f'For now, pre-commit does not support ' - f'additional_dependencies for {lang}', + f'for now, pre-commit does not support ' + f'additional_dependencies for {lang} -- ' + f'you selected `additional_dependencies: {additional_deps}`', ) diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index 49d81226e..259cb97c9 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -88,7 +88,9 @@ def test_assert_no_additional_deps(): helpers.assert_no_additional_deps('lang', ['hmmm']) msg, = excinfo.value.args assert msg == ( - 'For now, pre-commit does not support additional_dependencies for lang' + 'for now, pre-commit does not support additional_dependencies for ' + 'lang -- ' + "you selected `additional_dependencies: ['hmmm']`" ) From e8b46c1b1660ed5556b519ecd478ae829bd51d9c Mon Sep 17 00:00:00 2001 From: Matt Layman Date: Wed, 30 Mar 2022 01:08:52 -0400 Subject: [PATCH 615/967] Pick a tag if multiple tags exist on a SHA. Fixes #2311 --- pre_commit/commands/autoupdate.py | 3 +++ pre_commit/git.py | 15 +++++++++++++++ tests/commands/autoupdate_test.py | 18 ++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 938c22461..d5352e5e7 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -59,6 +59,9 @@ def update(self, tags_only: bool, freeze: bool) -> RevInfo: except CalledProcessError: cmd = (*git_cmd, 'rev-parse', 'FETCH_HEAD') rev = cmd_output(*cmd, cwd=tmp)[1].strip() + else: + if tags_only: + rev = git.get_best_candidate_tag(rev, tmp) frozen = None if freeze: diff --git a/pre_commit/git.py b/pre_commit/git.py index 853f4b0d0..6fff8d2a9 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -229,3 +229,18 @@ def check_for_cygwin_mismatch() -> None: f' - python {exe_type[is_cygwin_python]}\n' f' - git {exe_type[is_cygwin_git]}\n', ) + + +def get_best_candidate_tag(rev: str, git_repo: str) -> str: + """Get the best tag candidate. + + Multiple tags can exist on a SHA. Sometimes a moving tag is attached + to a version tag. Try to pick the tag that looks like a version. + """ + tags = cmd_output( + 'git', *NO_FS_MONITOR, 'tag', '--points-at', rev, cwd=git_repo, + )[1].splitlines() + for tag in tags: + if '.' in tag: + return tag + return rev diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 3a142661a..3806b0e48 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -103,6 +103,24 @@ def test_rev_info_update_tags_only_does_not_pick_tip(tagged): assert new_info.rev == 'v1.2.3' +def test_rev_info_update_tags_prefers_version_tag(tagged, out_of_date): + cmd_output('git', 'tag', 'latest', cwd=out_of_date.path) + config = make_config_from_repo(tagged.path, rev=tagged.original_rev) + info = RevInfo.from_config(config) + new_info = info.update(tags_only=True, freeze=False) + assert new_info.rev == 'v1.2.3' + + +def test_rev_info_update_tags_non_version_tag(out_of_date): + cmd_output('git', 'tag', 'latest', cwd=out_of_date.path) + config = make_config_from_repo( + out_of_date.path, rev=out_of_date.original_rev, + ) + info = RevInfo.from_config(config) + new_info = info.update(tags_only=True, freeze=False) + assert new_info.rev == 'latest' + + def test_rev_info_update_freeze_tag(tagged): git_commit(cwd=tagged.path) config = make_config_from_repo(tagged.path, rev=tagged.original_rev) From 9021fa15dd1925b59634d660c0e5d429cb757a52 Mon Sep 17 00:00:00 2001 From: Jamie Alessio Date: Thu, 31 Mar 2022 10:33:36 -0700 Subject: [PATCH 616/967] Update ruby-build to latest available --- pre_commit/resources/ruby-build.tar.gz | Bin 71151 -> 72271 bytes testing/make-archives | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz index 01867bee6112203f21d1b808451070f6ad56f5c0..e248c57ce6fc23ae3ac0f6794ba02f4270da89c1 100644 GIT binary patch literal 72271 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%0Ae!h-MdIj$Cs`t9_~$5Z8g+qK)~>k_$qCgd0G zjjnE`*|!;>l{+ARYLhJ8;eOjsQmNOgS6nHb^@?obeM{lbUe=Q<*(e$zcjp!?Ij;pp z7;#{M#wNnLZUaBkvDAx;rRaMd2S)w$f#A}>8EQP$;(p5=?L{nP=<^_>X6)9g=vQrb z+^8ZiS|00Tp)A^B$@Joll1QlFWmT0_?%DBHF5TfZk+OJR1XaQ!%E%}5N5znH_Q%KO ziYoCI6Y2dkV^T(o>gu}ez{YC=NBwYHot+s}=h>$B^r$*ceeUkLzdeW!4M*il28pP{ zj4w@|-G*4rRZPDp!ApF{u!;uvP)nq-O3U2Vm!ZDB>Ba>Kuvj_ZwH(T}Ha!j`g=pH> zfDcGzDn14>)g3V}O*a8ewwa!0Ms;xTFC=YBo+xdTsX#80gXbe`Jm*pQD9BM~xaJ;Y zdzb%34jg?BRxzpi1!^7fgMldLSIV%q-Kkh79AWqhf+RowI#mw^x>a#Lv+$dvRJCHw$$R{*u zu!TZ6Zb8E=NYX%$B_J{GjqdH)F!mJTL?hzH+bL&GsH9@!R*0B*N{Jy(Rn!+|T}Pa^!H4#>&(h57^?L1iwXHK1z=&}}*B9t_&LIfE}{fZL!x zO8_)KXlsZEhNM;Y2LW{eL~ldHNrV$&rO| z5ms5$QRD*)6ND}Ckw!kJrT{W&Rr4tJ!{?&R*HXRmx2I-pz`7}iw}wuwO1o*L^nq~a zmF#deTaD$H7_&s?Buklu_OBr2PS+O#op0LvSo>`rkGrA;?0xasG{*I3m`MmjL&eJ% zdUuZDWGBBO+?;18DRa!SEaACIX|Ncd>Hde)MOW-=eC5ph`sc{HDBavqcZ0EIck%1S zX~#IirxpF>qvs)o&%es5H~KW7YI|@CzEv zjclVAOC=;Ir|_Dj=xaNl{z$Y@uS^8|qaS2Q3z{CGNWKP^PmN(Rb<)6=+5xbcaHSGd zZsf{KLrMAG3ZG1|y-HI?NyvMzW1isK*Rr2ghs3fHU*1|cmgGWcknK|gC`%_AO9O8c zD^1B#H#VP9#7R4*j+Iqnb8Y&p+zejSw0TtTP9-oCl)EX33*ng4aD}~{xE@pQ*w`JJ zdxl>&FqL^a4BMEnaP<>!>_2Paoy(luRm3BQoHCwQy@IJ35MB|4OC)riWm z3<&99cInWxV9*kYob)0Sn@tx1p15Tfce|C<{p%8imE6{6_nj2I#o5c6x-fjMw&j~< zue(D`UhX4M;O|X8R+Mxoh(hm>5}~I_%~SoLziI&C*k8x3Y4Uo={s%cf5mX z&oZZb6GLtKM5_t62J79MKz|g0UX-vr-9U~z(`_%^4xnOt|bJYpwo#HN`(>jo8QTgZa%&e1YC_J;LFQ@X@2(s|~ohw(redWXJ z%SUL{VO!1+E^RGs*m50q)Tdt`Q@298D(&7{*p^F7ZAc$Suoha?ufCWa%O*b)5cf5+ z++XGSf3*U7s{Vh(^t`$Cf77Y=|DM75|Aj7p1qZ=752;q>ABA|-s`PPzp~3aT(%Py@jLUiNRYce~M<gGn?kll^*+xdyt zs`Ax@S%D<0wOW;zYo9(xD2dEj3OQYFP@<*sp_$VY-Ow+?NKODSvU7uChJ6K+Yqa3M zt=7YbgI2Se|2lAXyQ;+tLq6*h3rZ&s1a?EqQtUl9PndjE*X(SFm;kv28+8<2ez|6z z#n(~!qv|x?cmqQzP7T^R1!C!F*GC0+jp?N7*l66}mqs+iK&cpIFk!b52Y@vmex@>Q zVT^QinZc9)wjsxqzDBOHjmAAG)ge^#0;Fee9)70#O_9B=PNQ)nQu!xc>Dh3m0v@5Y zdeEC-2G5MyXm6}TqvlT1KyqM2$M#iYf%k(kn#Iu&kKp}B!vm=}tP9d-;EiW| zTI>Z{3Yy8udYPZ;J_4tzDY8IT8>?5iu{upku5n81oeTO`s*e}s$MKcysW2s; z&Om?}%L!OjIB~)IGjVpiO39qw1gnxso>f^DA3c)eal0K2$6Ug{lP`Lsb`t6tet#k5 z2>uzdDc^6X6OF^+L^jg1jZcT~covo%3}<`$1{?3q4!{hXsIyuMq^Pt=OJgM%Yt0Xp zmYdU=;w%mhAgf#zr$angc<}7hDyT4%l#z3_%1-u&PYhMmhU?hguM)~-RV!^rV-sVB z=Sk?=qG`pr%+6%nmkh^D7oY6+_hAvN}lV@xq-N%OMa(P z1!dCLYZb6{RchTrwYGYlRFHBz;7%Y)>DQ|ZQ~F6Ooy9EA|6!fi8vOgU6UG0ggx2w& zU&j2e=0E;DHU2xM>y+nz3C4e~=YL$Q8);+*UUsEmB zG#hvB-v03Jm5=c_pm+%;PSn(;#^dh2`w!mVy>s)*Ewz71XMUJHZN@3?KB!0l50m#dZe6?n$$Sd+IL+hwy$3h% zS7f7}C#kOAx;CE#9wzUvU4Q?GU8@Ge?9l+5SsrsPDTu}wX2kHII(QJ z`$p-DM$~IGlA-_W8A%RfN~%d1(kPV%G)WUY#@#Gi&vP&0>3iV7VR!3!T!F(jcKbHy z8>oNdf^h@K0<7QrQsTKudQD(oCIg27m?of057dUNPNP_eqBj&nO3yWk;e`qHeZAPG zT#fbdU*&hIQa6Xgk%3#o49P$hqT^69**EGls7P-hdV8!A z$A=~AR9hD>ZmBzcEVWvDRDAm~S2plm)$5Hn;7y!dpOl=Wh+CWEtYKU*zAN<+xoO>W zWuM5_7^^&$FiON-Sv{z<{^A}W(bO155l+p6LZ2PrR8(wH=tO#Cxuf=Vt4~IK;69cR zQ(`(apd>aPvuBV~&K{c%<4+E7xMbkgc9w2(pAHpA;UAC%F$3CbLqZoCr`l;6TUbg{ zJye8hZpH7K#_Kysk@#m9Or{b+3_RW&8jVMxrs=-!8L)U_2>9Z!mn45ch@_l4yoY%5QVrix-XFj`_CHduQwUh2+Ce-+OcjkKTOKxO6E!A~dZp z(l|}I)q9lkAjuvRq(#Lk5;n03cFyE2p5_+6F^q%=#wFwJx6`MW;b$zmBzrC=jH_F9 z3TeV8Jy)9vu98n7AInGl5g-6C6D$`npZgu-HlEG>q&ERR_lJ9$?|PWf z$z*p+S4j0`6Z&^SRu%JP{NrK*p{cN6N|I>}+1&ckX70nb{!r>Q7j;{Wf+Ue^^#`e7 zyfcULjVfglgHl@O3&vGEUC)?}RBN#_<8DuEF2-z#=CL$K>%r_G#1pC_1tOC?p5hdF zA9w+;r~6EoqzaJVnyjgScemT1R>M z;Hg+yyW$TUjhOIh`%0B9wJApe^-(1mkeF4CoNrlsZ<;jyn2r1{ZgxiCO<7pbu!iwG zu^0+kR%x+fEwnn*B3b+-E_i32YKC#BBd?UjBqnowRD1)?kveR~E9?K0^ZHQnYEB{k zhilXF{$Gz;b^NbWxnj>A7z}_Gn|Gv9qqBY!)l~I3(I4zojg7oDl0=cpf(mf2!f(!V`I8 z-zn{;qmSr45b1+k*T1Vv&^d2o)_UBO1Z|5`T-ZKrrh|p{<9MXn74mU+FzfeI^D-+3 zOkHE^;wssErL6GQ{n5K~oQvpufDkJg-=WfRTFXV9l(|`sU7(HZP+$X{6ccn20;*P!;zaoVSUes^L&> zqS1V?7XP#z7fX=cwF5Rv=zQKt*NpwDaK*D`P7rD?WKA$b9HU$c}8c zw+jB`s!}B%_Cj?)8dNkK4Yg_zK`yipV}(eRI*CyYlkO<_nHZY$ai26uBO5pCY*8H* z8;A-2@5g0+SA$OJUQ}%83m+Ab;bf|`C#XMA^IE&}W!0kda8T#w&6#Luh z$UTAL_nO%t{V-Nm&B?YFqwRz&{hI>NfTfq^RGy1Di%XhZ6#@%*`AlV3Z<*Rl{HIp&nCLFJPQ*Rni|#1%Ia`W zJ<*)Z0c!Ejb%3}5r42e?fmJwtHj=2a_f()W_2(p<Z`u$tG?>1zUr&K>Z`u$tG?>1zUr&K>Z`u$tG?>1zUr&K c>Z`u$tG?>1zUr&KUjFs}0dqxwA^->s0GV&_9{>OV literal 71151 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 Date: Tue, 22 Mar 2022 21:52:46 -0400 Subject: [PATCH 617/967] use Rscript path relative to $R_HOME/bin/... Co-authored-by: Lorenz Walthert --- pre_commit/languages/r.py | 6 +++++- tests/languages/r_test.py | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 9b32b2d74..c736b3863 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -59,7 +59,11 @@ def _prefix_if_non_local_file_entry( def _rscript_exec() -> str: - return os.path.join(os.getenv('R_HOME', ''), 'Rscript') + r_home = os.environ.get('R_HOME') + if r_home is None: + return 'Rscript' + else: + return os.path.join(r_home, 'bin', 'Rscript') def _entry_validate(entry: Sequence[str]) -> None: diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py index bc302a795..5bc63b27c 100644 --- a/tests/languages/r_test.py +++ b/tests/languages/r_test.py @@ -4,6 +4,7 @@ import pytest +from pre_commit import envcontext from pre_commit.languages import r from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo @@ -129,3 +130,14 @@ def test_r_parsing_file_local(tempdir_factory, store): config=config, expect_path_prefix=False, ) + + +def test_rscript_exec_relative_to_r_home(): + expected = os.path.join('r_home_dir', 'bin', 'Rscript') + with envcontext.envcontext((('R_HOME', 'r_home_dir'),)): + assert r._rscript_exec() == expected + + +def test_path_rscript_exec_no_r_home_set(): + with envcontext.envcontext((('R_HOME', envcontext.UNSET),)): + assert r._rscript_exec() == 'Rscript' From ba132f02007f6d185a33a80c2d537f4d29335c04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Sun, 11 Jul 2021 07:59:33 +0200 Subject: [PATCH 618/967] Split get_git_dir() into get_git_dir() and get_git_common_dir() This fixes the conflicted state check when using work trees. #1972 --- pre_commit/commands/install_uninstall.py | 2 +- pre_commit/git.py | 26 ++++++++++++++------ tests/git_test.py | 30 ++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index cb2aaa5bf..b1fd8d490 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -36,7 +36,7 @@ def _hook_paths( hook_type: str, git_dir: str | None = None, ) -> tuple[str, str]: - git_dir = git_dir if git_dir is not None else git.get_git_dir() + git_dir = git_dir if git_dir is not None else git.get_git_common_dir() pth = os.path.join(git_dir, 'hooks', hook_type) return pth, f'{pth}.legacy' diff --git a/pre_commit/git.py b/pre_commit/git.py index 6fff8d2a9..35392b341 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -57,13 +57,15 @@ def get_root() -> str: root = os.path.abspath( cmd_output('git', 'rev-parse', '--show-cdup')[1].strip(), ) - git_dir = os.path.abspath(get_git_dir()) + inside_git_dir = cmd_output( + 'git', 'rev-parse', '--is-inside-git-dir', + )[1].strip() except CalledProcessError: raise FatalError( 'git failed. Is it installed, and are you in a Git repository ' 'directory?', ) - if os.path.samefile(root, git_dir): + if inside_git_dir != 'false': raise FatalError( 'git toplevel unexpectedly empty! make sure you are not ' 'inside the `.git` directory of your repository.', @@ -72,15 +74,25 @@ def get_root() -> str: def get_git_dir(git_root: str = '.') -> str: - opts = ('--git-common-dir', '--git-dir') - _, out, _ = cmd_output('git', 'rev-parse', *opts, cwd=git_root) - for line, opt in zip(out.splitlines(), opts): - if line != opt: # pragma: no branch (git < 2.5) - return os.path.normpath(os.path.join(git_root, line)) + opt = '--git-dir' + _, out, _ = cmd_output('git', 'rev-parse', opt, cwd=git_root) + git_dir = out.strip() + if git_dir != opt: + return os.path.normpath(os.path.join(git_root, git_dir)) else: raise AssertionError('unreachable: no git dir') +def get_git_common_dir(git_root: str = '.') -> str: + opt = '--git-common-dir' + _, out, _ = cmd_output('git', 'rev-parse', opt, cwd=git_root) + git_common_dir = out.strip() + if git_common_dir != opt: + return os.path.normpath(os.path.join(git_root, git_common_dir)) + else: # pragma: no cover (git < 2.5) + return get_git_dir(git_root) + + def get_remote_url(git_root: str) -> str: _, out, _ = cmd_output('git', 'config', 'remote.origin.url', cwd=git_root) return out.strip() diff --git a/tests/git_test.py b/tests/git_test.py index d9e497c5b..b9f524a14 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -21,6 +21,20 @@ def test_get_root_deeper(in_git_dir): assert os.path.normcase(git.get_root()) == expected +def test_get_root_in_git_sub_dir(in_git_dir): + expected = os.path.normcase(in_git_dir.strpath) + with pytest.raises(FatalError): + with in_git_dir.join('.git/objects').ensure_dir().as_cwd(): + assert os.path.normcase(git.get_root()) == expected + + +def test_get_root_not_in_working_dir(in_git_dir): + expected = os.path.normcase(in_git_dir.strpath) + with pytest.raises(FatalError): + with in_git_dir.join('..').ensure_dir().as_cwd(): + assert os.path.normcase(git.get_root()) == expected + + def test_in_exactly_dot_git(in_git_dir): with in_git_dir.join('.git').as_cwd(), pytest.raises(FatalError): git.get_root() @@ -40,6 +54,22 @@ def test_get_root_bare_worktree(tmpdir): assert git.get_root() == os.path.abspath('.') +def test_get_git_dir(tmpdir): + """Regression test for #1972""" + src = tmpdir.join('src').ensure_dir() + cmd_output('git', 'init', str(src)) + git_commit(cwd=str(src)) + + worktree = tmpdir.join('worktree').ensure_dir() + cmd_output('git', 'worktree', 'add', '../worktree', cwd=src) + + with worktree.as_cwd(): + assert git.get_git_dir() == src.ensure_dir( + '.git/worktrees/worktree', + ) + assert git.get_git_common_dir() == src.ensure_dir('.git') + + def test_get_root_worktree_in_git(tmpdir): src = tmpdir.join('src').ensure_dir() cmd_output('git', 'init', str(src)) From fd0177ae3ae5f94b36aafb54ab496f76fcead7b9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 2 Apr 2022 14:00:23 -0400 Subject: [PATCH 619/967] implement default_install_hook_types this implements a configurable fallback for the default value of `pre-commit install` --- pre_commit/clientlib.py | 6 +++ pre_commit/commands/init_templatedir.py | 3 +- pre_commit/commands/install_uninstall.py | 22 +++++++--- pre_commit/constants.py | 6 +++ pre_commit/main.py | 34 +++------------ tests/commands/install_uninstall_test.py | 54 +++++++++++++++++++++--- tests/main_test.py | 16 +------ 7 files changed, 86 insertions(+), 55 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 1fcce4eaf..bf4e2e455 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -336,6 +336,11 @@ def check(self, dct: dict[str, Any]) -> None: 'Config', None, cfgv.RequiredRecurse('repos', cfgv.Array(CONFIG_REPO_DICT)), + cfgv.Optional( + 'default_install_hook_types', + cfgv.check_array(cfgv.check_one_of(C.HOOK_TYPES)), + ['pre-commit'], + ), cfgv.OptionalRecurse( 'default_language_version', DEFAULT_LANGUAGE_VERSION, {}, ), @@ -355,6 +360,7 @@ def check(self, dct: dict[str, Any]) -> None: cfgv.WarnAdditionalKeys( ( 'repos', + 'default_install_hook_types', 'default_language_version', 'default_stages', 'files', diff --git a/pre_commit/commands/init_templatedir.py b/pre_commit/commands/init_templatedir.py index 004e8ccfc..08af6561e 100644 --- a/pre_commit/commands/init_templatedir.py +++ b/pre_commit/commands/init_templatedir.py @@ -2,7 +2,6 @@ import logging import os.path -from typing import Sequence from pre_commit.commands.install_uninstall import install from pre_commit.store import Store @@ -16,7 +15,7 @@ def init_templatedir( config_file: str, store: Store, directory: str, - hook_types: Sequence[str], + hook_types: list[str] | None, skip_on_missing_config: bool = True, ) -> int: install( diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index cb2aaa5bf..c80e17e5f 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -5,10 +5,10 @@ import shlex import shutil import sys -from typing import Sequence from pre_commit import git from pre_commit import output +from pre_commit.clientlib import InvalidConfigError from pre_commit.clientlib import load_config from pre_commit.repository import all_hooks from pre_commit.repository import install_hook_envs @@ -32,6 +32,18 @@ TEMPLATE_END = '# end templated\n' +def _hook_types(cfg_filename: str, hook_types: list[str] | None) -> list[str]: + if hook_types is not None: + return hook_types + else: + try: + cfg = load_config(cfg_filename) + except InvalidConfigError: + return ['pre-commit'] + else: + return cfg['default_install_hook_types'] + + def _hook_paths( hook_type: str, git_dir: str | None = None, @@ -103,7 +115,7 @@ def _install_hook_script( def install( config_file: str, store: Store, - hook_types: Sequence[str], + hook_types: list[str] | None, overwrite: bool = False, hooks: bool = False, skip_on_missing_config: bool = False, @@ -116,7 +128,7 @@ def install( ) return 1 - for hook_type in hook_types: + for hook_type in _hook_types(config_file, hook_types): _install_hook_script( config_file, hook_type, overwrite=overwrite, @@ -150,7 +162,7 @@ def _uninstall_hook_script(hook_type: str) -> None: output.write_line(f'Restored previous hooks to {hook_path}') -def uninstall(hook_types: Sequence[str]) -> int: - for hook_type in hook_types: +def uninstall(config_file: str, hook_types: list[str] | None) -> int: + for hook_type in _hook_types(config_file, hook_types): _uninstall_hook_script(hook_type) return 0 diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 40127a052..5bc4ae98b 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -24,4 +24,10 @@ 'post-rewrite', ) +HOOK_TYPES = ( + 'pre-commit', 'pre-merge-commit', 'pre-push', 'prepare-commit-msg', + 'commit-msg', 'post-commit', 'post-checkout', 'post-merge', + 'post-rewrite', +) + DEFAULT = 'default' diff --git a/pre_commit/main.py b/pre_commit/main.py index f3c551885..645e97f74 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -4,7 +4,6 @@ import logging import os import sys -from typing import Any from typing import Sequence import pre_commit.constants as C @@ -46,34 +45,10 @@ def _add_config_option(parser: argparse.ArgumentParser) -> None: ) -class AppendReplaceDefault(argparse.Action): - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.appended = False - - def __call__( - self, - parser: argparse.ArgumentParser, - namespace: argparse.Namespace, - values: str | Sequence[str] | None, - option_string: str | None = None, - ) -> None: - if not self.appended: - setattr(namespace, self.dest, []) - self.appended = True - getattr(namespace, self.dest).append(values) - - def _add_hook_type_option(parser: argparse.ArgumentParser) -> None: parser.add_argument( - '-t', '--hook-type', choices=( - 'pre-commit', 'pre-merge-commit', 'pre-push', 'prepare-commit-msg', - 'commit-msg', 'post-commit', 'post-checkout', 'post-merge', - 'post-rewrite', - ), - action=AppendReplaceDefault, - default=['pre-commit'], - dest='hook_types', + '-t', '--hook-type', + choices=C.HOOK_TYPES, action='append', dest='hook_types', ) @@ -399,7 +374,10 @@ def main(argv: Sequence[str] | None = None) -> int: elif args.command == 'try-repo': return try_repo(args) elif args.command == 'uninstall': - return uninstall(hook_types=args.hook_types) + return uninstall( + config_file=args.config, + hook_types=args.hook_types, + ) else: raise NotImplementedError( f'Command {args.command} not implemented.', diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 703ba03b7..ae668ac9f 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -7,6 +7,7 @@ import pre_commit.constants as C from pre_commit import git +from pre_commit.commands.install_uninstall import _hook_types from pre_commit.commands.install_uninstall import CURRENT_HASH from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import install_hooks @@ -27,6 +28,36 @@ from testing.util import git_commit +def test_hook_types_explicitly_listed(): + assert _hook_types(os.devnull, ['pre-push']) == ['pre-push'] + + +def test_hook_types_default_value_when_not_specified(): + assert _hook_types(os.devnull, None) == ['pre-commit'] + + +def test_hook_types_configured(tmpdir): + cfg = tmpdir.join('t.cfg') + cfg.write('default_install_hook_types: [pre-push]\nrepos: []\n') + + assert _hook_types(str(cfg), None) == ['pre-push'] + + +def test_hook_types_configured_nonsense(tmpdir): + cfg = tmpdir.join('t.cfg') + cfg.write('default_install_hook_types: []\nrepos: []\n') + + # hopefully the user doesn't do this, but the code allows it! + assert _hook_types(str(cfg), None) == [] + + +def test_hook_types_configuration_has_error(tmpdir): + cfg = tmpdir.join('t.cfg') + cfg.write('[') + + assert _hook_types(str(cfg), None) == ['pre-commit'] + + def test_is_not_script(): assert is_our_script('setup.py') is False @@ -61,7 +92,7 @@ def test_install_multiple_hooks_at_once(in_git_dir, store): install(C.CONFIG_FILE, store, hook_types=['pre-commit', 'pre-push']) assert in_git_dir.join('.git/hooks/pre-commit').exists() assert in_git_dir.join('.git/hooks/pre-push').exists() - uninstall(hook_types=['pre-commit', 'pre-push']) + uninstall(C.CONFIG_FILE, hook_types=['pre-commit', 'pre-push']) assert not in_git_dir.join('.git/hooks/pre-commit').exists() assert not in_git_dir.join('.git/hooks/pre-push').exists() @@ -79,14 +110,14 @@ def test_install_hooks_dead_symlink(in_git_dir, store): def test_uninstall_does_not_blow_up_when_not_there(in_git_dir): - assert uninstall(hook_types=['pre-commit']) == 0 + assert uninstall(C.CONFIG_FILE, hook_types=['pre-commit']) == 0 def test_uninstall(in_git_dir, store): assert not in_git_dir.join('.git/hooks/pre-commit').exists() install(C.CONFIG_FILE, store, hook_types=['pre-commit']) assert in_git_dir.join('.git/hooks/pre-commit').exists() - uninstall(hook_types=['pre-commit']) + uninstall(C.CONFIG_FILE, hook_types=['pre-commit']) assert not in_git_dir.join('.git/hooks/pre-commit').exists() @@ -416,7 +447,7 @@ def test_uninstall_restores_legacy_hooks(tempdir_factory, store): # Now install and uninstall pre-commit assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 - assert uninstall(hook_types=['pre-commit']) == 0 + assert uninstall(C.CONFIG_FILE, hook_types=['pre-commit']) == 0 # Make sure we installed the "old" hook correctly ret, output = _get_commit_output(tempdir_factory, touch_file='baz') @@ -451,7 +482,7 @@ def test_uninstall_doesnt_remove_not_our_hooks(in_git_dir): pre_commit.write('#!/usr/bin/env bash\necho 1\n') make_executable(pre_commit.strpath) - assert uninstall(hook_types=['pre-commit']) == 0 + assert uninstall(C.CONFIG_FILE, hook_types=['pre-commit']) == 0 assert pre_commit.exists() @@ -1007,3 +1038,16 @@ def test_install_temporarily_allow_mising_config(tempdir_factory, store): 'Skipping `pre-commit`.' ) assert expected in output + + +def test_install_uninstall_default_hook_types(in_git_dir, store): + cfg_src = 'default_install_hook_types: [pre-commit, pre-push]\nrepos: []\n' + in_git_dir.join(C.CONFIG_FILE).write(cfg_src) + + assert not install(C.CONFIG_FILE, store, hook_types=None) + assert os.access(in_git_dir.join('.git/hooks/pre-commit').strpath, os.X_OK) + assert os.access(in_git_dir.join('.git/hooks/pre-push').strpath, os.X_OK) + + assert not uninstall(C.CONFIG_FILE, hook_types=None) + assert not in_git_dir.join('.git/hooks/pre-commit').exists() + assert not in_git_dir.join('.git/hooks/pre-push').exists() diff --git a/tests/main_test.py b/tests/main_test.py index 64b26a00c..a645300ab 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -14,20 +14,6 @@ from testing.util import cwd -@pytest.mark.parametrize( - ('argv', 'expected'), - ( - ((), ['f']), - (('--f', 'x'), ['x']), - (('--f', 'x', '--f', 'y'), ['x', 'y']), - ), -) -def test_append_replace_default(argv, expected): - parser = argparse.ArgumentParser() - parser.add_argument('--f', action=main.AppendReplaceDefault, default=['f']) - assert parser.parse_args(argv).f == expected - - def _args(**kwargs): kwargs.setdefault('command', 'help') kwargs.setdefault('config', C.CONFIG_FILE) @@ -172,7 +158,7 @@ def test_init_templatedir(mock_store_dir): assert patch.call_count == 1 assert 'tdir' in patch.call_args[0] - assert patch.call_args[1]['hook_types'] == ['pre-commit'] + assert patch.call_args[1]['hook_types'] is None assert patch.call_args[1]['skip_on_missing_config'] is True From a138c85e64cb9ba7703565650504cff0bb339505 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 2 Apr 2022 16:20:16 -0400 Subject: [PATCH 620/967] move patch discarding inside `try` for staged_files_only there's a rare race outlined in #2287 --- pre_commit/staged_files_only.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 7e75080d4..83d8a03e1 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -66,9 +66,9 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: # prevent recursive post-checkout hooks (#1418) no_checkout_env = dict(os.environ, _PRE_COMMIT_SKIP_POST_CHECKOUT='1') - cmd_output_b(*_CHECKOUT_CMD, env=no_checkout_env) try: + cmd_output_b(*_CHECKOUT_CMD, env=no_checkout_env) yield finally: # Try to apply the patch we saved From c5a39ae77e1aff1df32034b27a3a900473698d46 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 2 Apr 2022 19:36:45 -0400 Subject: [PATCH 621/967] v2.18.0 --- CHANGELOG.md | 43 +++++++++++++++++++++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0cccc6da..e59917222 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,46 @@ +2.18.0 - 2022-04-02 +=================== + +### Features +- Keep `GIT_HTTP_PROXY_AUTHMETHOD` in git environ. + - #2272 PR by @VincentBerthier. + - #2271 issue by @VincentBerthier. +- Support both `cs` and `coursier` executables for coursier hooks. + - #2293 PR by @Holzhaus. +- Include more information in errors for `language_version` / + `additional_dependencies` for languages which do not support them. + - #2315 PR by @asottile. +- Have autoupdate preferentially pick tags which look like versions when + there are multiple equivalent tags. + - #2312 PR by @mblayman. + - #2311 issue by @mblayman. +- Upgrade `ruby-build`. + - #2319 PR by @jalessio. +- Add top level `default_install_hook_types` which will be installed when + `--hook-types` is not specified in `pre-commit install`. + - #2322 PR by @asottile. + +### Fixes +- Fix typo in help message for `--from-ref` and `--to-ref`. + - #2266 PR by @leetrout. +- Prioritize binary builds for R dependencies. + - #2277 PR by @lorenzwalthert. +- Fix handling of git worktrees. + - #2252 PR by @daschuer. +- Fix handling of `$R_HOME` for R hooks. + - #2301 PR by @jeff-m-sullivan. + - #2300 issue by @jeff-m-sullivan. +- Fix a rare race condition in change stashing. + - #2323 PR by @asottile. + - #2287 issue by @ian-h-chamberlain. + +### Updating +- Remove python3.6 support. Note that pre-commit still supports running hooks + written in older versions, but pre-commit itself requires python 3.7+. + - #2215 PR by @asottile. +- pre-commit has migrated from the `master` branch to `main`. + - #2302 PR by @asottile. + 2.17.0 - 2022-01-18 =================== diff --git a/setup.cfg b/setup.cfg index d712a3f30..92b154593 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.17.0 +version = 2.18.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 1722448c3b26abdeb80f403ebe5f3171249e655f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 2 Apr 2022 20:26:03 -0400 Subject: [PATCH 622/967] fix python 2.7 `repo: local` hooks --- .pre-commit-config.yaml | 2 +- pre_commit/resources/empty_template_setup.py | 2 -- .../python3_hooks_repo/.pre-commit-hooks.yaml | 1 - tests/repository_test.py | 21 ++++++++++++------- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5525d71d7..1b93cff54 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: rev: v3.0.1 hooks: - id: reorder-python-imports - exclude: ^testing/resources/python3_hooks_repo/ + exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) args: [--py37-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma rev: v2.2.1 diff --git a/pre_commit/resources/empty_template_setup.py b/pre_commit/resources/empty_template_setup.py index 870d0fbae..ef05eef84 100644 --- a/pre_commit/resources/empty_template_setup.py +++ b/pre_commit/resources/empty_template_setup.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from setuptools import setup diff --git a/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml index 2c2370092..964cf8363 100644 --- a/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml +++ b/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml @@ -2,5 +2,4 @@ name: Python 3 Hook entry: python3-hook language: python - language_version: python3 files: \.py$ diff --git a/tests/repository_test.py b/tests/repository_test.py index 013731476..cef688716 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -2,7 +2,6 @@ import os.path import shutil -import sys from typing import Any from unittest import mock @@ -876,7 +875,7 @@ def test_tags_on_repositories(in_tmpdir, tempdir_factory, store): @pytest.fixture def local_python_config(): # Make a "local" hooks repo that just installs our other hooks repo - repo_path = get_resource_path('python_hooks_repo') + repo_path = get_resource_path('python3_hooks_repo') manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) hooks = [ dict(hook, additional_dependencies=[repo_path]) for hook in manifest @@ -884,17 +883,23 @@ def local_python_config(): return {'repo': 'local', 'hooks': hooks} -@pytest.mark.xfail( # pragma: win32 no cover - sys.platform == 'win32', - reason='microsoft/azure-pipelines-image-generation#989', -) def test_local_python_repo(store, local_python_config): - hook = _get_hook(local_python_config, store, 'foo') + hook = _get_hook(local_python_config, store, 'python3-hook') + # language_version should have been adjusted to the interpreter version + assert hook.language_version != C.DEFAULT + ret, out = _hook_run(hook, ('filename',), color=False) + assert ret == 0 + assert _norm_out(out) == b"3\n['filename']\nHello World\n" + + +def test_local_python_repo_python2(store, local_python_config): + local_python_config['hooks'][0]['language_version'] = 'python2' + hook = _get_hook(local_python_config, store, 'python3-hook') # language_version should have been adjusted to the interpreter version assert hook.language_version != C.DEFAULT ret, out = _hook_run(hook, ('filename',), color=False) assert ret == 0 - assert _norm_out(out) == b"['filename']\nHello World\n" + assert _norm_out(out) == b"2\n['filename']\nHello World\n" def test_default_language_version(store, local_python_config): From 0276e25f713ddfd66482f358d238186ca47a6eb4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 2 Apr 2022 21:32:54 -0400 Subject: [PATCH 623/967] v2.18.1 --- CHANGELOG.md | 7 +++++++ setup.cfg | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e59917222..cd31c4b17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +2.18.1 - 2022-04-02 +=================== + +### Fixes +- Fix regression for `repo: local` hooks running `python<3.7` + - #2324 PR by @asottile. + 2.18.0 - 2022-04-02 =================== diff --git a/setup.cfg b/setup.cfg index 92b154593..ca92af3e7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.18.0 +version = 2.18.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From c8ce94b40ec7280517ed46b7808e1612f9ee5c40 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Apr 2022 20:00:42 +0000 Subject: [PATCH 624/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v1.20.0 → v1.20.1](https://github.com/asottile/setup-cfg-fmt/compare/v1.20.0...v1.20.1) - [github.com/asottile/add-trailing-comma: v2.2.1 → v2.2.2](https://github.com/asottile/add-trailing-comma/compare/v2.2.1...v2.2.2) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1b93cff54..9fff994a6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.20.0 + rev: v1.20.1 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder_python_imports @@ -20,7 +20,7 @@ repos: exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) args: [--py37-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma - rev: v2.2.1 + rev: v2.2.2 hooks: - id: add-trailing-comma args: [--py36-plus] From 9b3df4b90e287ae99658f684dd489eea3d0defd9 Mon Sep 17 00:00:00 2001 From: Walluce Pinkham Date: Wed, 6 Apr 2022 10:19:21 +0100 Subject: [PATCH 625/967] Handling multiple outputs from dotnet pack --- pre_commit/languages/dotnet.py | 27 ++++++++---------- .../.pre-commit-hooks.yaml | 12 ++++++++ .../dotnet_hooks_combo_repo.sln | 28 +++++++++++++++++++ .../dotnet_hooks_combo_repo/proj1/Program.cs | 12 ++++++++ .../proj1/proj1.csproj | 12 ++++++++ .../dotnet_hooks_combo_repo/proj2/Program.cs | 12 ++++++++ .../proj2/proj2.csproj | 12 ++++++++ tests/repository_test.py | 1 + 8 files changed, 101 insertions(+), 15 deletions(-) create mode 100644 testing/resources/dotnet_hooks_combo_repo/.pre-commit-hooks.yaml create mode 100644 testing/resources/dotnet_hooks_combo_repo/dotnet_hooks_combo_repo.sln create mode 100644 testing/resources/dotnet_hooks_combo_repo/proj1/Program.cs create mode 100644 testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj create mode 100644 testing/resources/dotnet_hooks_combo_repo/proj2/Program.cs create mode 100644 testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index a16e7f077..9323f4079 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -59,22 +59,19 @@ def install_environment( # Determine tool from the packaged file ..nupkg build_outputs = os.listdir(os.path.join(prefix.prefix_dir, build_dir)) - if len(build_outputs) != 1: - raise NotImplementedError( - f"Can't handle multiple build outputs. Got {build_outputs}", + for output in build_outputs: + tool_name = output.split('.')[0] + + # Install to bin dir + helpers.run_setup_cmd( + prefix, + ( + 'dotnet', 'tool', 'install', + '--tool-path', os.path.join(envdir, BIN_DIR), + '--add-source', build_dir, + tool_name, + ), ) - tool_name = build_outputs[0].split('.')[0] - - # Install to bin dir - helpers.run_setup_cmd( - prefix, - ( - 'dotnet', 'tool', 'install', - '--tool-path', os.path.join(envdir, BIN_DIR), - '--add-source', build_dir, - tool_name, - ), - ) # Clean the git dir, ignoring the environment dir clean_cmd = ('git', 'clean', '-ffxd', '-e', f'{ENVIRONMENT_DIR}-*') diff --git a/testing/resources/dotnet_hooks_combo_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_combo_repo/.pre-commit-hooks.yaml new file mode 100644 index 000000000..f221854a4 --- /dev/null +++ b/testing/resources/dotnet_hooks_combo_repo/.pre-commit-hooks.yaml @@ -0,0 +1,12 @@ +- id: dotnet-example-hook + name: Test Project 1 + description: Test Project 1 + entry: proj1 + language: dotnet + stages: [commit] +- id: proj2 + name: Test Project 2 + description: Test Project 2 + entry: proj2 + language: dotnet + stages: [commit] diff --git a/testing/resources/dotnet_hooks_combo_repo/dotnet_hooks_combo_repo.sln b/testing/resources/dotnet_hooks_combo_repo/dotnet_hooks_combo_repo.sln new file mode 100644 index 000000000..edb0fcbc5 --- /dev/null +++ b/testing/resources/dotnet_hooks_combo_repo/dotnet_hooks_combo_repo.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "proj1", "proj1\proj1.csproj", "{38A939C3-DEA4-47D7-9B75-0418C4249662}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "proj2", "proj2\proj2.csproj", "{4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Release|Any CPU.Build.0 = Release|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/testing/resources/dotnet_hooks_combo_repo/proj1/Program.cs b/testing/resources/dotnet_hooks_combo_repo/proj1/Program.cs new file mode 100644 index 000000000..03876f5cd --- /dev/null +++ b/testing/resources/dotnet_hooks_combo_repo/proj1/Program.cs @@ -0,0 +1,12 @@ +using System; + +namespace proj1 +{ + class Program + { + static void Main(string[] args) + { + Console.Write("Hello from dotnet!\n"); + } + } +} diff --git a/testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj b/testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj new file mode 100644 index 000000000..4f714d339 --- /dev/null +++ b/testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj @@ -0,0 +1,12 @@ + + + + Exe + net5.0 + + true + proj1 + ./nupkg + + + diff --git a/testing/resources/dotnet_hooks_combo_repo/proj2/Program.cs b/testing/resources/dotnet_hooks_combo_repo/proj2/Program.cs new file mode 100644 index 000000000..47a99a358 --- /dev/null +++ b/testing/resources/dotnet_hooks_combo_repo/proj2/Program.cs @@ -0,0 +1,12 @@ +using System; + +namespace proj2 +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + } + } +} diff --git a/testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj b/testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj new file mode 100644 index 000000000..da451f7cc --- /dev/null +++ b/testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj @@ -0,0 +1,12 @@ + + + + Exe + net5.0 + + true + proj2 + ./nupkg + + + diff --git a/tests/repository_test.py b/tests/repository_test.py index cef688716..5c7909c5c 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1042,6 +1042,7 @@ def test_local_perl_additional_dependencies(store): ( 'dotnet_hooks_csproj_repo', 'dotnet_hooks_sln_repo', + 'dotnet_hooks_combo_repo', ), ) def test_dotnet_hook(tempdir_factory, store, repo): From e3ae0664bb44445e49d16bdc0358f003cb78ed3e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Apr 2022 20:54:45 +0000 Subject: [PATCH 626/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.1.0 → v4.2.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.1.0...v4.2.0) - [github.com/asottile/pyupgrade: v2.31.1 → v2.32.0](https://github.com/asottile/pyupgrade/compare/v2.31.1...v2.32.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9fff994a6..887772b86 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.1.0 + rev: v4.2.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -25,7 +25,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.31.1 + rev: v2.32.0 hooks: - id: pyupgrade args: [--py37-plus] From b952c99898bacea651b4e39c8be57d987f64c094 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 13 Apr 2022 17:52:55 -0400 Subject: [PATCH 627/967] fix tests for golang 1.18 --- testing/resources/golang_hooks_repo/go.mod | 4 ++++ testing/resources/golang_hooks_repo/go.sum | 2 ++ tests/repository_test.py | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 testing/resources/golang_hooks_repo/go.sum diff --git a/testing/resources/golang_hooks_repo/go.mod b/testing/resources/golang_hooks_repo/go.mod index 523bfc9f5..f37d4b674 100644 --- a/testing/resources/golang_hooks_repo/go.mod +++ b/testing/resources/golang_hooks_repo/go.mod @@ -1 +1,5 @@ module golang-hello-world + +go 1.18 + +require github.com/BurntSushi/toml v1.1.0 diff --git a/testing/resources/golang_hooks_repo/go.sum b/testing/resources/golang_hooks_repo/go.sum new file mode 100644 index 000000000..ec0c385a0 --- /dev/null +++ b/testing/resources/golang_hooks_repo/go.sum @@ -0,0 +1,2 @@ +github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= +github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= diff --git a/tests/repository_test.py b/tests/repository_test.py index 5c7909c5c..cfa69c9fd 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -667,7 +667,7 @@ def test_additional_golang_dependencies_installed( path = make_repo(tempdir_factory, 'golang_hooks_repo') config = make_config_from_repo(path) # A small go package - deps = ['golang.org/x/example/hello'] + deps = ['golang.org/x/example/hello@latest'] config['hooks'][0]['additional_dependencies'] = deps hook = _get_hook(config, store, 'golang-hook') binaries = os.listdir( @@ -688,7 +688,7 @@ def test_local_golang_additional_dependencies(store): 'name': 'hello', 'entry': 'hello', 'language': 'golang', - 'additional_dependencies': ['golang.org/x/example/hello'], + 'additional_dependencies': ['golang.org/x/example/hello@latest'], }], } hook = _get_hook(config, store, 'hello') From feb0d342130c94908a8ecc63f919ee023c9c18af Mon Sep 17 00:00:00 2001 From: Wade Carpenter Date: Thu, 14 Apr 2022 14:27:46 -0700 Subject: [PATCH 628/967] pre-push: fix stdin line splitting when has whitespace From the `pre-push.sample` file: > Information about the commits which are being pushed is supplied as > lines to the standard input in the form: > > When `` is not simply a branch name, but a more general ref (see git-rev-parse(1)), it could contain whitespace, and that breaks the split() call that expected only 3 spaces in the line. Changed to use `rsplit(maxsplit=3)` since only the is likely to have embedded whitespace. Added a new test case for the same. --- pre_commit/commands/hook_impl.py | 3 ++- tests/commands/hook_impl_test.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index 18e5e9f59..f315c04de 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -114,7 +114,8 @@ def _pre_push_ns( remote_url = args[1] for line in stdin.decode().splitlines(): - local_branch, local_sha, remote_branch, remote_sha = line.split() + parts = line.rsplit(maxsplit=3) + local_branch, local_sha, remote_branch, remote_sha = parts if local_sha == Z40: continue elif remote_sha != Z40 and _rev_exists(remote_sha): diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index b0159f8e3..3e20874e3 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -242,6 +242,18 @@ def test_run_ns_pre_push_new_branch_existing_rev(push_example): assert ns is None +def test_run_ns_pre_push_ref_with_whitespace(push_example): + src, src_head, clone, _ = push_example + + with cwd(clone): + args = ('origin', src) + line = f'HEAD^{{/ }} {src_head} refs/heads/b2 {hook_impl.Z40}\n' + stdin = line.encode() + ns = hook_impl._run_ns('pre-push', False, args, stdin) + + assert ns is None + + def test_pushing_orphan_branch(push_example): src, src_head, clone, _ = push_example From 07554e952539e9a85e6e8c33987108911d49fb15 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 16 Apr 2022 14:59:46 -0400 Subject: [PATCH 629/967] add additional info to healthy-after-install check --- CONTRIBUTING.md | 7 ++-- pre_commit/languages/all.py | 40 ++++++++++----------- pre_commit/languages/conda.py | 2 +- pre_commit/languages/coursier.py | 2 +- pre_commit/languages/dart.py | 2 +- pre_commit/languages/docker.py | 2 +- pre_commit/languages/docker_image.py | 2 +- pre_commit/languages/dotnet.py | 2 +- pre_commit/languages/fail.py | 2 +- pre_commit/languages/golang.py | 2 +- pre_commit/languages/helpers.py | 4 +-- pre_commit/languages/lua.py | 2 +- pre_commit/languages/node.py | 7 ++-- pre_commit/languages/perl.py | 2 +- pre_commit/languages/pygrep.py | 2 +- pre_commit/languages/python.py | 35 +++++++++++++----- pre_commit/languages/r.py | 2 +- pre_commit/languages/ruby.py | 2 +- pre_commit/languages/rust.py | 2 +- pre_commit/languages/script.py | 2 +- pre_commit/languages/swift.py | 2 +- pre_commit/languages/system.py | 2 +- pre_commit/repository.py | 10 +++--- testing/gen-languages-all | 12 +++---- tests/languages/helpers_test.py | 4 +-- tests/languages/node_test.py | 9 ++--- tests/languages/python_test.py | 54 ++++++++++++++++++++++------ 27 files changed, 137 insertions(+), 79 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index adce08f91..fa1678ca1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -117,15 +117,16 @@ get_default_version = helpers.basic_default_version `python` is currently the only language which implements this api -#### `healthy` +#### `health_check` This is used to check whether the installed environment is considered healthy. -This function should return `True` or `False`. +This function should return a detailed message if unhealthy or `None` if +healthy. You generally don't need to implement this on a first pass and can just use: ```python -healthy = helpers.basic_healthy +health_check = helpers.basic_healthy_check ``` `python` is currently the only language which implements this api, for python diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index cfcbf686b..cfd42ce20 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -34,7 +34,7 @@ class Language(NamedTuple): # return a value to replace `'default` for `language_version` get_default_version: Callable[[], str] # return whether the environment is healthy (or should be rebuilt) - healthy: Callable[[Prefix, str], bool] + health_check: Callable[[Prefix, str], str | None] # install a repository for the given language and language_version install_environment: Callable[[Prefix, str, Sequence[str]], None] # execute a hook and return the exit code and output @@ -44,25 +44,25 @@ class Language(NamedTuple): # TODO: back to modules + Protocol: https://github.com/python/mypy/issues/5018 languages = { # BEGIN GENERATED (testing/gen-languages-all) - 'conda': Language(name='conda', ENVIRONMENT_DIR=conda.ENVIRONMENT_DIR, get_default_version=conda.get_default_version, healthy=conda.healthy, install_environment=conda.install_environment, run_hook=conda.run_hook), # noqa: E501 - 'coursier': Language(name='coursier', ENVIRONMENT_DIR=coursier.ENVIRONMENT_DIR, get_default_version=coursier.get_default_version, healthy=coursier.healthy, install_environment=coursier.install_environment, run_hook=coursier.run_hook), # noqa: E501 - 'dart': Language(name='dart', ENVIRONMENT_DIR=dart.ENVIRONMENT_DIR, get_default_version=dart.get_default_version, healthy=dart.healthy, install_environment=dart.install_environment, run_hook=dart.run_hook), # noqa: E501 - 'docker': Language(name='docker', ENVIRONMENT_DIR=docker.ENVIRONMENT_DIR, get_default_version=docker.get_default_version, healthy=docker.healthy, install_environment=docker.install_environment, run_hook=docker.run_hook), # noqa: E501 - 'docker_image': Language(name='docker_image', ENVIRONMENT_DIR=docker_image.ENVIRONMENT_DIR, get_default_version=docker_image.get_default_version, healthy=docker_image.healthy, install_environment=docker_image.install_environment, run_hook=docker_image.run_hook), # noqa: E501 - 'dotnet': Language(name='dotnet', ENVIRONMENT_DIR=dotnet.ENVIRONMENT_DIR, get_default_version=dotnet.get_default_version, healthy=dotnet.healthy, install_environment=dotnet.install_environment, run_hook=dotnet.run_hook), # noqa: E501 - 'fail': Language(name='fail', ENVIRONMENT_DIR=fail.ENVIRONMENT_DIR, get_default_version=fail.get_default_version, healthy=fail.healthy, install_environment=fail.install_environment, run_hook=fail.run_hook), # noqa: E501 - 'golang': Language(name='golang', ENVIRONMENT_DIR=golang.ENVIRONMENT_DIR, get_default_version=golang.get_default_version, healthy=golang.healthy, install_environment=golang.install_environment, run_hook=golang.run_hook), # noqa: E501 - 'lua': Language(name='lua', ENVIRONMENT_DIR=lua.ENVIRONMENT_DIR, get_default_version=lua.get_default_version, healthy=lua.healthy, install_environment=lua.install_environment, run_hook=lua.run_hook), # noqa: E501 - 'node': Language(name='node', ENVIRONMENT_DIR=node.ENVIRONMENT_DIR, get_default_version=node.get_default_version, healthy=node.healthy, install_environment=node.install_environment, run_hook=node.run_hook), # noqa: E501 - 'perl': Language(name='perl', ENVIRONMENT_DIR=perl.ENVIRONMENT_DIR, get_default_version=perl.get_default_version, healthy=perl.healthy, install_environment=perl.install_environment, run_hook=perl.run_hook), # noqa: E501 - 'pygrep': Language(name='pygrep', ENVIRONMENT_DIR=pygrep.ENVIRONMENT_DIR, get_default_version=pygrep.get_default_version, healthy=pygrep.healthy, install_environment=pygrep.install_environment, run_hook=pygrep.run_hook), # noqa: E501 - 'python': Language(name='python', ENVIRONMENT_DIR=python.ENVIRONMENT_DIR, get_default_version=python.get_default_version, healthy=python.healthy, install_environment=python.install_environment, run_hook=python.run_hook), # noqa: E501 - 'r': Language(name='r', ENVIRONMENT_DIR=r.ENVIRONMENT_DIR, get_default_version=r.get_default_version, healthy=r.healthy, install_environment=r.install_environment, run_hook=r.run_hook), # noqa: E501 - 'ruby': Language(name='ruby', ENVIRONMENT_DIR=ruby.ENVIRONMENT_DIR, get_default_version=ruby.get_default_version, healthy=ruby.healthy, install_environment=ruby.install_environment, run_hook=ruby.run_hook), # noqa: E501 - 'rust': Language(name='rust', ENVIRONMENT_DIR=rust.ENVIRONMENT_DIR, get_default_version=rust.get_default_version, healthy=rust.healthy, install_environment=rust.install_environment, run_hook=rust.run_hook), # noqa: E501 - 'script': Language(name='script', ENVIRONMENT_DIR=script.ENVIRONMENT_DIR, get_default_version=script.get_default_version, healthy=script.healthy, install_environment=script.install_environment, run_hook=script.run_hook), # noqa: E501 - 'swift': Language(name='swift', ENVIRONMENT_DIR=swift.ENVIRONMENT_DIR, get_default_version=swift.get_default_version, healthy=swift.healthy, install_environment=swift.install_environment, run_hook=swift.run_hook), # noqa: E501 - 'system': Language(name='system', ENVIRONMENT_DIR=system.ENVIRONMENT_DIR, get_default_version=system.get_default_version, healthy=system.healthy, install_environment=system.install_environment, run_hook=system.run_hook), # noqa: E501 + 'conda': Language(name='conda', ENVIRONMENT_DIR=conda.ENVIRONMENT_DIR, get_default_version=conda.get_default_version, health_check=conda.health_check, install_environment=conda.install_environment, run_hook=conda.run_hook), # noqa: E501 + 'coursier': Language(name='coursier', ENVIRONMENT_DIR=coursier.ENVIRONMENT_DIR, get_default_version=coursier.get_default_version, health_check=coursier.health_check, install_environment=coursier.install_environment, run_hook=coursier.run_hook), # noqa: E501 + 'dart': Language(name='dart', ENVIRONMENT_DIR=dart.ENVIRONMENT_DIR, get_default_version=dart.get_default_version, health_check=dart.health_check, install_environment=dart.install_environment, run_hook=dart.run_hook), # noqa: E501 + 'docker': Language(name='docker', ENVIRONMENT_DIR=docker.ENVIRONMENT_DIR, get_default_version=docker.get_default_version, health_check=docker.health_check, install_environment=docker.install_environment, run_hook=docker.run_hook), # noqa: E501 + 'docker_image': Language(name='docker_image', ENVIRONMENT_DIR=docker_image.ENVIRONMENT_DIR, get_default_version=docker_image.get_default_version, health_check=docker_image.health_check, install_environment=docker_image.install_environment, run_hook=docker_image.run_hook), # noqa: E501 + 'dotnet': Language(name='dotnet', ENVIRONMENT_DIR=dotnet.ENVIRONMENT_DIR, get_default_version=dotnet.get_default_version, health_check=dotnet.health_check, install_environment=dotnet.install_environment, run_hook=dotnet.run_hook), # noqa: E501 + 'fail': Language(name='fail', ENVIRONMENT_DIR=fail.ENVIRONMENT_DIR, get_default_version=fail.get_default_version, health_check=fail.health_check, install_environment=fail.install_environment, run_hook=fail.run_hook), # noqa: E501 + 'golang': Language(name='golang', ENVIRONMENT_DIR=golang.ENVIRONMENT_DIR, get_default_version=golang.get_default_version, health_check=golang.health_check, install_environment=golang.install_environment, run_hook=golang.run_hook), # noqa: E501 + 'lua': Language(name='lua', ENVIRONMENT_DIR=lua.ENVIRONMENT_DIR, get_default_version=lua.get_default_version, health_check=lua.health_check, install_environment=lua.install_environment, run_hook=lua.run_hook), # noqa: E501 + 'node': Language(name='node', ENVIRONMENT_DIR=node.ENVIRONMENT_DIR, get_default_version=node.get_default_version, health_check=node.health_check, install_environment=node.install_environment, run_hook=node.run_hook), # noqa: E501 + 'perl': Language(name='perl', ENVIRONMENT_DIR=perl.ENVIRONMENT_DIR, get_default_version=perl.get_default_version, health_check=perl.health_check, install_environment=perl.install_environment, run_hook=perl.run_hook), # noqa: E501 + 'pygrep': Language(name='pygrep', ENVIRONMENT_DIR=pygrep.ENVIRONMENT_DIR, get_default_version=pygrep.get_default_version, health_check=pygrep.health_check, install_environment=pygrep.install_environment, run_hook=pygrep.run_hook), # noqa: E501 + 'python': Language(name='python', ENVIRONMENT_DIR=python.ENVIRONMENT_DIR, get_default_version=python.get_default_version, health_check=python.health_check, install_environment=python.install_environment, run_hook=python.run_hook), # noqa: E501 + 'r': Language(name='r', ENVIRONMENT_DIR=r.ENVIRONMENT_DIR, get_default_version=r.get_default_version, health_check=r.health_check, install_environment=r.install_environment, run_hook=r.run_hook), # noqa: E501 + 'ruby': Language(name='ruby', ENVIRONMENT_DIR=ruby.ENVIRONMENT_DIR, get_default_version=ruby.get_default_version, health_check=ruby.health_check, install_environment=ruby.install_environment, run_hook=ruby.run_hook), # noqa: E501 + 'rust': Language(name='rust', ENVIRONMENT_DIR=rust.ENVIRONMENT_DIR, get_default_version=rust.get_default_version, health_check=rust.health_check, install_environment=rust.install_environment, run_hook=rust.run_hook), # noqa: E501 + 'script': Language(name='script', ENVIRONMENT_DIR=script.ENVIRONMENT_DIR, get_default_version=script.get_default_version, health_check=script.health_check, install_environment=script.install_environment, run_hook=script.run_hook), # noqa: E501 + 'swift': Language(name='swift', ENVIRONMENT_DIR=swift.ENVIRONMENT_DIR, get_default_version=swift.get_default_version, health_check=swift.health_check, install_environment=swift.install_environment, run_hook=swift.run_hook), # noqa: E501 + 'system': Language(name='system', ENVIRONMENT_DIR=system.ENVIRONMENT_DIR, get_default_version=system.get_default_version, health_check=system.health_check, install_environment=system.install_environment, run_hook=system.run_hook), # noqa: E501 # END GENERATED } # TODO: fully deprecate `python_venv` diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index 88ac53f33..f0195e4f7 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -18,7 +18,7 @@ ENVIRONMENT_DIR = 'conda' get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check def get_env_patch(env: str) -> PatchesT: diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index bb3e0b848..9fe43ebd8 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -17,7 +17,7 @@ ENVIRONMENT_DIR = 'coursier' get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check def install_environment( diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py index 65135f80a..55ecbf4fd 100644 --- a/pre_commit/languages/dart.py +++ b/pre_commit/languages/dart.py @@ -21,7 +21,7 @@ ENVIRONMENT_DIR = 'dartenv' get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check def get_env_patch(venv: str) -> PatchesT: diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index af1860c5b..eea9f7682 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -16,7 +16,7 @@ ENVIRONMENT_DIR = 'docker' PRE_COMMIT_LABEL = 'PRE_COMMIT' get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check def _is_in_docker() -> bool: diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index ccc1d6782..daa4d1ba3 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -8,7 +8,7 @@ ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check install_environment = helpers.no_install diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index 9323f4079..3983c6f0c 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -18,7 +18,7 @@ BIN_DIR = 'bin' get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check def get_env_patch(venv: str) -> PatchesT: diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index 4cb95af5a..00b06a9a9 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -7,7 +7,7 @@ ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check install_environment = helpers.no_install diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 759c26849..a5f9dba02 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -21,7 +21,7 @@ ENVIRONMENT_DIR = 'golangenv' get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check def get_env_patch(venv: str) -> PatchesT: diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 808082665..05a71651e 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -88,8 +88,8 @@ def basic_get_default_version() -> str: return C.DEFAULT -def basic_healthy(prefix: Prefix, language_version: str) -> bool: - return True +def basic_health_check(prefix: Prefix, language_version: str) -> str | None: + return None def no_install( diff --git a/pre_commit/languages/lua.py b/pre_commit/languages/lua.py index 38bdf54b8..49aa7308c 100644 --- a/pre_commit/languages/lua.py +++ b/pre_commit/languages/lua.py @@ -18,7 +18,7 @@ ENVIRONMENT_DIR = 'lua_env' get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check def _get_lua_version() -> str: # pragma: win32 no cover diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index b084e8f89..39f300065 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -73,10 +73,13 @@ def in_env( yield -def healthy(prefix: Prefix, language_version: str) -> bool: +def health_check(prefix: Prefix, language_version: str) -> str | None: with in_env(prefix, language_version): retcode, _, _ = cmd_output_b('node', '--version', retcode=None) - return retcode == 0 + if retcode != 0: # pragma: win32 no cover + return f'`node --version` returned {retcode}' + else: + return None def install_environment( diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py index 0eee258d7..78bd65a2b 100644 --- a/pre_commit/languages/perl.py +++ b/pre_commit/languages/perl.py @@ -16,7 +16,7 @@ ENVIRONMENT_DIR = 'perl_env' get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check def _envdir(prefix: Prefix, version: str) -> str: diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index f2758c588..2e2072b08 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -14,7 +14,7 @@ ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check install_environment = helpers.no_install diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 668ba3587..19fa247ef 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -163,27 +163,44 @@ def in_env( yield -def healthy(prefix: Prefix, language_version: str) -> bool: +def health_check(prefix: Prefix, language_version: str) -> str | None: directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version) envdir = prefix.path(directory) pyvenv_cfg = os.path.join(envdir, 'pyvenv.cfg') # created with "old" virtualenv if not os.path.exists(pyvenv_cfg): - return False + return 'pyvenv.cfg does not exist (old virtualenv?)' exe_name = win_exe('python') py_exe = prefix.path(bin_dir(envdir), exe_name) cfg = _read_pyvenv_cfg(pyvenv_cfg) - return ( - 'version_info' in cfg and - # always use uncached lookup here in case we replaced an unhealthy env - _version_info.__wrapped__(py_exe) == cfg['version_info'] and ( - 'base-executable' not in cfg or - _version_info(cfg['base-executable']) == cfg['version_info'] + if 'version_info' not in cfg: + return "created virtualenv's pyvenv.cfg is missing `version_info`" + + # always use uncached lookup here in case we replaced an unhealthy env + virtualenv_version = _version_info.__wrapped__(py_exe) + if virtualenv_version != cfg['version_info']: + return ( + f'virtualenv python version did not match created version:\n' + f'- actual version: {virtualenv_version}\n' + f'- expected version: {cfg["version_info"]}\n' ) - ) + + # made with an older version of virtualenv? skip `base-executable` check + if 'base-executable' not in cfg: + return None + + base_exe_version = _version_info(cfg['base-executable']) + if base_exe_version != cfg['version_info']: + return ( + f'base executable python version does not match created version:\n' + f'- base-executable version: {base_exe_version}\n' + f'- expected version: {cfg["version_info"]}\n' + ) + else: + return None def install_environment( diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index c736b3863..40a001dbf 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -19,7 +19,7 @@ ENVIRONMENT_DIR = 'renv' RSCRIPT_OPTS = ('--no-save', '--no-restore', '--no-site-file', '--no-environ') get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check def get_env_patch(venv: str) -> PatchesT: diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index ae6449270..6c5cff280 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -21,7 +21,7 @@ from pre_commit.util import resource_bytesio ENVIRONMENT_DIR = 'rbenv' -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check @functools.lru_cache(maxsize=1) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 39e36281c..01c373061 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -19,7 +19,7 @@ ENVIRONMENT_DIR = 'rustenv' get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check def get_env_patch(target_dir: str) -> PatchesT: diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 2844b5e5d..d5e7677f9 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -7,7 +7,7 @@ ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check install_environment = helpers.no_install diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index c6309531e..4c687030c 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -17,7 +17,7 @@ ENVIRONMENT_DIR = 'swift_env' get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check BUILD_DIR = '.build' BUILD_CONFIG = 'release' diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 9846c98ba..c64fb3650 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -8,7 +8,7 @@ ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check install_environment = helpers.no_install diff --git a/pre_commit/repository.py b/pre_commit/repository.py index ac5d294bd..4092277a8 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -57,7 +57,7 @@ def _hook_installed(hook: Hook) -> bool: _read_state(hook.prefix, venv) == _state(hook.additional_dependencies) ) and - lang.healthy(hook.prefix, hook.language_version) + not lang.health_check(hook.prefix, hook.language_version) ) ) @@ -79,11 +79,13 @@ def _hook_install(hook: Hook) -> None: lang.install_environment( hook.prefix, hook.language_version, hook.additional_dependencies, ) - if not lang.healthy(hook.prefix, hook.language_version): + health_error = lang.health_check(hook.prefix, hook.language_version) + if health_error: raise AssertionError( - f'BUG: expected environment for {hook.language} to be healthy() ' + f'BUG: expected environment for {hook.language} to be healthy ' f'immediately after install, please open an issue describing ' - f'your environment', + f'your environment\n\n' + f'more info:\n\n{health_error}', ) # Write our state to indicate we're installed _write_state(hook.prefix, venv, _state(hook.additional_dependencies)) diff --git a/testing/gen-languages-all b/testing/gen-languages-all index dfd92c0ed..05f892956 100755 --- a/testing/gen-languages-all +++ b/testing/gen-languages-all @@ -3,15 +3,15 @@ from __future__ import annotations import sys -LANGUAGES = [ +LANGUAGES = ( 'conda', 'coursier', 'dart', 'docker', 'docker_image', 'dotnet', 'fail', 'golang', 'lua', 'node', 'perl', 'pygrep', 'python', 'r', 'ruby', 'rust', 'script', 'swift', 'system', -] -FIELDS = [ - 'ENVIRONMENT_DIR', 'get_default_version', 'healthy', 'install_environment', - 'run_hook', -] +) +FIELDS = ( + 'ENVIRONMENT_DIR', 'get_default_version', 'health_check', + 'install_environment', 'run_hook', +) def main() -> int: diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index 259cb97c9..f333e79d5 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -67,8 +67,8 @@ def test_basic_get_default_version(): assert helpers.basic_get_default_version() == C.DEFAULT -def test_basic_healthy(): - assert helpers.basic_healthy(Prefix('.'), 'default') is True +def test_basic_health_check(): + assert helpers.basic_health_check(Prefix('.'), 'default') is None def test_failed_setup_command_does_not_unicode_error(): diff --git a/tests/languages/node_test.py b/tests/languages/node_test.py index fb5ae7172..b69adfa67 100644 --- a/tests/languages/node_test.py +++ b/tests/languages/node_test.py @@ -62,7 +62,7 @@ def test_healthy_system_node(tmpdir): prefix = Prefix(str(tmpdir)) node.install_environment(prefix, 'system', ()) - assert node.healthy(prefix, 'system') + assert node.health_check(prefix, 'system') is None @xfailif_windows # pragma: win32 no cover @@ -78,10 +78,11 @@ def test_unhealthy_if_system_node_goes_missing(tmpdir): with envcontext.envcontext((path,)): prefix = Prefix(str(prefix_dir)) node.install_environment(prefix, 'system', ()) - assert node.healthy(prefix, 'system') + assert node.health_check(prefix, 'system') is None node_bin.remove() - assert not node.healthy(prefix, 'system') + ret = node.health_check(prefix, 'system') + assert ret == '`node --version` returned 127' @xfailif_windows # pragma: win32 no cover @@ -101,7 +102,7 @@ def test_installs_without_links_outside_env(tmpdir): prefix = Prefix(str(tmpdir)) node.install_environment(prefix, 'system', ()) - assert node.healthy(prefix, 'system') + assert node.health_check(prefix, 'system') is None # this directory shouldn't exist, make sure we succeed without it existing cmd_output('rm', '-rf', str(tmpdir.join('node_modules'))) diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 616066965..54fb98feb 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -93,11 +93,11 @@ def test_healthy_default_creator(python_dir): python.install_environment(prefix, C.DEFAULT, ()) # should be healthy right after creation - assert python.healthy(prefix, C.DEFAULT) is True + assert python.health_check(prefix, C.DEFAULT) is None # even if a `types.py` file exists, should still be healthy tmpdir.join('types.py').ensure() - assert python.healthy(prefix, C.DEFAULT) is True + assert python.health_check(prefix, C.DEFAULT) is None def test_healthy_venv_creator(python_dir): @@ -107,7 +107,7 @@ def test_healthy_venv_creator(python_dir): with envcontext((('VIRTUALENV_CREATOR', 'venv'),)): python.install_environment(prefix, C.DEFAULT, ()) - assert python.healthy(prefix, C.DEFAULT) is True + assert python.health_check(prefix, C.DEFAULT) is None def test_unhealthy_python_goes_missing(python_dir): @@ -119,7 +119,12 @@ def test_unhealthy_python_goes_missing(python_dir): py_exe = prefix.path(python.bin_dir('py_env-default'), exe_name) os.remove(py_exe) - assert python.healthy(prefix, C.DEFAULT) is False + ret = python.health_check(prefix, C.DEFAULT) + assert ret == ( + f'virtualenv python version did not match created version:\n' + f'- actual version: <>\n' + f'- expected version: {python._version_info(sys.executable)}\n' + ) def test_unhealthy_with_version_change(python_dir): @@ -127,10 +132,15 @@ def test_unhealthy_with_version_change(python_dir): python.install_environment(prefix, C.DEFAULT, ()) - with open(prefix.path('py_env-default/pyvenv.cfg'), 'w') as f: + with open(prefix.path('py_env-default/pyvenv.cfg'), 'a+') as f: f.write('version_info = 1.2.3\n') - assert python.healthy(prefix, C.DEFAULT) is False + ret = python.health_check(prefix, C.DEFAULT) + assert ret == ( + f'virtualenv python version did not match created version:\n' + f'- actual version: {python._version_info(sys.executable)}\n' + f'- expected version: 1.2.3\n' + ) def test_unhealthy_system_version_changes(python_dir): @@ -141,7 +151,12 @@ def test_unhealthy_system_version_changes(python_dir): with open(prefix.path('py_env-default/pyvenv.cfg'), 'a') as f: f.write('base-executable = /does/not/exist\n') - assert python.healthy(prefix, C.DEFAULT) is False + ret = python.health_check(prefix, C.DEFAULT) + assert ret == ( + f'base executable python version does not match created version:\n' + f'- base-executable version: <>\n' # noqa: E501 + f'- expected version: {python._version_info(sys.executable)}\n' + ) def test_unhealthy_old_virtualenv(python_dir): @@ -152,7 +167,21 @@ def test_unhealthy_old_virtualenv(python_dir): # simulate "old" virtualenv by deleting this file os.remove(prefix.path('py_env-default/pyvenv.cfg')) - assert python.healthy(prefix, C.DEFAULT) is False + ret = python.health_check(prefix, C.DEFAULT) + assert ret == 'pyvenv.cfg does not exist (old virtualenv?)' + + +def test_unhealthy_unexpected_pyvenv(python_dir): + prefix, tmpdir = python_dir + + python.install_environment(prefix, C.DEFAULT, ()) + + # simulate a buggy environment build (I don't think this is possible) + with open(prefix.path('py_env-default/pyvenv.cfg'), 'w'): + pass + + ret = python.health_check(prefix, C.DEFAULT) + assert ret == "created virtualenv's pyvenv.cfg is missing `version_info`" def test_unhealthy_then_replaced(python_dir): @@ -170,9 +199,14 @@ def test_unhealthy_then_replaced(python_dir): make_executable(py_exe) # should be unhealthy due to version mismatch - assert python.healthy(prefix, C.DEFAULT) is False + ret = python.health_check(prefix, C.DEFAULT) + assert ret == ( + f'virtualenv python version did not match created version:\n' + f'- actual version: 1.2.3\n' + f'- expected version: {python._version_info(sys.executable)}\n' + ) # now put the exe back and it should be healthy again os.replace(f'{py_exe}.tmp', py_exe) - assert python.healthy(prefix, C.DEFAULT) is True + assert python.health_check(prefix, C.DEFAULT) is None From 392bc33466178dfe32ddcbfb728f398ff0fbf72e Mon Sep 17 00:00:00 2001 From: Jamie Alessio Date: Wed, 13 Apr 2022 10:19:57 -0700 Subject: [PATCH 630/967] Update ruby-build to v20220412 --- pre_commit/resources/ruby-build.tar.gz | Bin 72271 -> 72569 bytes testing/make-archives | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz index e248c57ce6fc23ae3ac0f6794ba02f4270da89c1..8edb3caa3c27f1ccff9f0e0dc36e9863845c4efa 100644 GIT binary patch delta 63322 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

WnuW-a|gW*evUot6IZ=HCs#VwrXbV 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 U`cr@E&-wZMe+y?<_GWk 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 diff --git a/testing/make-archives b/testing/make-archives index 1b825fe05..04b42dd9d 100755 --- a/testing/make-archives +++ b/testing/make-archives @@ -17,7 +17,7 @@ from typing import Sequence REPOS = ( ('rbenv', 'https://github.com/rbenv/rbenv', '38e1fbb'), - ('ruby-build', 'https://github.com/rbenv/ruby-build', 'a5ca3e4'), + ('ruby-build', 'https://github.com/rbenv/ruby-build', '2004fd7'), ( 'ruby-download', 'https://github.com/garnieretienne/rvm-download', From 18f1cdf47078ee949c830468a189995e3ba534ba Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Apr 2022 19:50:30 +0000 Subject: [PATCH 631/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/add-trailing-comma: v2.2.2 → v2.2.3](https://github.com/asottile/add-trailing-comma/compare/v2.2.2...v2.2.3) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 887772b86..4d8dd1e2a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) args: [--py37-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma - rev: v2.2.2 + rev: v2.2.3 hooks: - id: add-trailing-comma args: [--py36-plus] From 777ffdd692b81db2432feccf8de16a6407a1d12a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 24 Apr 2022 18:04:19 -0400 Subject: [PATCH 632/967] deprecate pre-commit-validate-{config,manifest} --- .pre-commit-hooks.yaml | 4 ++-- pre_commit/clientlib.py | 30 +++++++++++------------- pre_commit/commands/validate_config.py | 16 +++++++++++++ pre_commit/commands/validate_manifest.py | 16 +++++++++++++ pre_commit/main.py | 26 ++++++++++++++++++-- tests/clientlib_test.py | 16 +++++++++++-- tests/main_test.py | 1 + 7 files changed, 87 insertions(+), 22 deletions(-) create mode 100644 pre_commit/commands/validate_config.py create mode 100644 pre_commit/commands/validate_manifest.py diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 3d1ffbcbb..e1aaf5830 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -1,6 +1,6 @@ - id: validate_manifest name: validate pre-commit manifest description: This validator validates a pre-commit hooks manifest file - entry: pre-commit-validate-manifest + entry: pre-commit validate-manifest language: python - files: ^(\.pre-commit-hooks\.yaml|hooks\.yaml)$ + files: ^\.pre-commit-hooks\.yaml$ diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index bf4e2e455..9b53e8107 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -14,6 +14,8 @@ import pre_commit.constants as C from pre_commit.color import add_color_option +from pre_commit.commands.validate_config import validate_config +from pre_commit.commands.validate_manifest import validate_manifest from pre_commit.errors import FatalError from pre_commit.languages.all import all_languages from pre_commit.logging_handler import logging_handler @@ -100,14 +102,12 @@ def validate_manifest_main(argv: Sequence[str] | None = None) -> int: args = parser.parse_args(argv) with logging_handler(args.color): - ret = 0 - for filename in args.filenames: - try: - load_manifest(filename) - except InvalidManifestError as e: - print(e) - ret = 1 - return ret + logger.warning( + 'pre-commit-validate-manifest is deprecated -- ' + 'use `pre-commit validate-manifest` instead.', + ) + + return validate_manifest(args.filenames) LOCAL = 'local' @@ -409,11 +409,9 @@ def validate_config_main(argv: Sequence[str] | None = None) -> int: args = parser.parse_args(argv) with logging_handler(args.color): - ret = 0 - for filename in args.filenames: - try: - load_config(filename) - except InvalidConfigError as e: - print(e) - ret = 1 - return ret + logger.warning( + 'pre-commit-validate-config is deprecated -- ' + 'use `pre-commit validate-config` instead.', + ) + + return validate_config(args.filenames) diff --git a/pre_commit/commands/validate_config.py b/pre_commit/commands/validate_config.py new file mode 100644 index 000000000..91bb017a3 --- /dev/null +++ b/pre_commit/commands/validate_config.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from pre_commit import clientlib + + +def validate_config(filenames: list[str]) -> int: + ret = 0 + + for filename in filenames: + try: + clientlib.load_config(filename) + except clientlib.InvalidConfigError as e: + print(e) + ret = 1 + + return ret diff --git a/pre_commit/commands/validate_manifest.py b/pre_commit/commands/validate_manifest.py new file mode 100644 index 000000000..372a6380f --- /dev/null +++ b/pre_commit/commands/validate_manifest.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from pre_commit import clientlib + + +def validate_manifest(filenames: list[str]) -> int: + ret = 0 + + for filename in filenames: + try: + clientlib.load_manifest(filename) + except clientlib.InvalidManifestError as e: + print(e) + ret = 1 + + return ret diff --git a/pre_commit/main.py b/pre_commit/main.py index 645e97f74..6d2814b37 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -21,6 +21,8 @@ from pre_commit.commands.run import run from pre_commit.commands.sample_config import sample_config from pre_commit.commands.try_repo import try_repo +from pre_commit.commands.validate_config import validate_config +from pre_commit.commands.validate_manifest import validate_manifest from pre_commit.error_handler import error_handler from pre_commit.logging_handler import logging_handler from pre_commit.store import Store @@ -34,8 +36,10 @@ # pyvenv os.environ.pop('__PYVENV_LAUNCHER__', None) - -COMMANDS_NO_GIT = {'clean', 'gc', 'init-templatedir', 'sample-config'} +COMMANDS_NO_GIT = { + 'clean', 'gc', 'init-templatedir', 'sample-config', + 'validate-config', 'validate-manifest', +} def _add_config_option(parser: argparse.ArgumentParser) -> None: @@ -304,6 +308,20 @@ def main(argv: Sequence[str] | None = None) -> int: _add_config_option(uninstall_parser) _add_hook_type_option(uninstall_parser) + validate_config_parser = subparsers.add_parser( + 'validate-config', help='Validate .pre-commit-config.yaml files', + ) + add_color_option(validate_config_parser) + _add_config_option(validate_config_parser) + validate_config_parser.add_argument('filenames', nargs='*') + + validate_manifest_parser = subparsers.add_parser( + 'validate-manifest', help='Validate .pre-commit-hooks.yaml files', + ) + add_color_option(validate_manifest_parser) + _add_config_option(validate_manifest_parser) + validate_manifest_parser.add_argument('filenames', nargs='*') + help = subparsers.add_parser( 'help', help='Show help for a specific command.', ) @@ -378,6 +396,10 @@ def main(argv: Sequence[str] | None = None) -> int: config_file=args.config, hook_types=args.hook_types, ) + elif args.command == 'validate-config': + return validate_config(args.filenames) + elif args.command == 'validate-manifest': + return validate_manifest(args.filenames) else: raise NotImplementedError( f'Command {args.command} not implemented.', diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 3fb3af523..fb36bb55a 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -122,8 +122,8 @@ def test_validate_config_old_list_format_ok(tmpdir, cap_out): f = tmpdir.join('cfg.yaml') f.write('- {repo: meta, hooks: [{id: identity}]}') assert not validate_config_main((f.strpath,)) - start = '[WARNING] normalizing pre-commit configuration to a top-level map' - assert cap_out.get().startswith(start) + msg = '[WARNING] normalizing pre-commit configuration to a top-level map' + assert msg in cap_out.get() def test_validate_warn_on_unknown_keys_at_repo_level(tmpdir, caplog): @@ -139,6 +139,12 @@ def test_validate_warn_on_unknown_keys_at_repo_level(tmpdir, caplog): ret_val = validate_config_main((f.strpath,)) assert not ret_val assert caplog.record_tuples == [ + ( + 'pre_commit', + logging.WARNING, + 'pre-commit-validate-config is deprecated -- ' + 'use `pre-commit validate-config` instead.', + ), ( 'pre_commit', logging.WARNING, @@ -162,6 +168,12 @@ def test_validate_warn_on_unknown_keys_at_top_level(tmpdir, caplog): ret_val = validate_config_main((f.strpath,)) assert not ret_val assert caplog.record_tuples == [ + ( + 'pre_commit', + logging.WARNING, + 'pre-commit-validate-config is deprecated -- ' + 'use `pre-commit validate-config` instead.', + ), ( 'pre_commit', logging.WARNING, diff --git a/tests/main_test.py b/tests/main_test.py index a645300ab..a7afd6da4 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -79,6 +79,7 @@ def test_adjust_args_try_repo_repo_relative(in_git_dir): FNS = ( 'autoupdate', 'clean', 'gc', 'hook_impl', 'install', 'install_hooks', 'migrate_config', 'run', 'sample_config', 'uninstall', + 'validate_config', 'validate_manifest', ) CMDS = tuple(fn.replace('_', '-') for fn in FNS) From 3929fe4a6323e68ee5e6f9a185a18bbacfd311b9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 24 Apr 2022 19:09:05 -0400 Subject: [PATCH 633/967] upgrade CI to ubuntu-latest / windows-latest --- azure-pipelines.yml | 2 +- testing/get-swift.sh | 6 +++--- tests/repository_test.py | 2 ++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index afb298289..454f6f137 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -10,7 +10,7 @@ resources: type: github endpoint: github name: asottile/azure-pipeline-templates - ref: refs/tags/v2.1.0 + ref: refs/tags/v2.4.1 jobs: - template: job--python-tox.yml@asottile diff --git a/testing/get-swift.sh b/testing/get-swift.sh index a05b7b9e6..b77e18c0e 100755 --- a/testing/get-swift.sh +++ b/testing/get-swift.sh @@ -3,9 +3,9 @@ set -euo pipefail . /etc/lsb-release -if [ "$DISTRIB_CODENAME" = "bionic" ]; then - SWIFT_URL='https://swift.org/builds/swift-5.1.3-release/ubuntu1804/swift-5.1.3-RELEASE/swift-5.1.3-RELEASE-ubuntu18.04.tar.gz' - SWIFT_HASH='ac82ccd773fe3d586fc340814e31e120da1ff695c6a712f6634e9cc720769610' +if [ "$DISTRIB_CODENAME" = "focal" ]; then + SWIFT_URL='https://download.swift.org/swift-5.6.1-release/ubuntu2004/swift-5.6.1-RELEASE/swift-5.6.1-RELEASE-ubuntu20.04.tar.gz' + SWIFT_HASH='2b4f22d4a8b59fe8e050f0b7f020f8d8f12553cbda56709b2340a4a3bb90cfea' else echo "unknown dist: ${DISTRIB_CODENAME}" 1>&2 exit 1 diff --git a/tests/repository_test.py b/tests/repository_test.py index cfa69c9fd..3729ab1d7 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -173,6 +173,7 @@ def test_python_venv(tempdir_factory, store): ) +@xfailif_windows # pragma: win32 no cover # no python 2 in GHA def test_switch_language_versions_doesnt_clobber(tempdir_factory, store): # We're using the python3 repo because it prints the python version path = make_repo(tempdir_factory, 'python3_hooks_repo') @@ -892,6 +893,7 @@ def test_local_python_repo(store, local_python_config): assert _norm_out(out) == b"3\n['filename']\nHello World\n" +@xfailif_windows # pragma: win32 no cover # no python2 in GHA def test_local_python_repo_python2(store, local_python_config): local_python_config['hooks'][0]['language_version'] = 'python2' hook = _get_hook(local_python_config, store, 'python3-hook') From 81129cefa550ad01d3b485f6a3015201948d33c8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 May 2022 20:18:58 +0000 Subject: [PATCH 634/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder_python_imports: v3.0.1 → v3.1.0](https://github.com/asottile/reorder_python_imports/compare/v3.0.1...v3.1.0) - [github.com/pre-commit/mirrors-mypy: v0.942 → v0.950](https://github.com/pre-commit/mirrors-mypy/compare/v0.942...v0.950) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4d8dd1e2a..7791f7656 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder_python_imports - rev: v3.0.1 + rev: v3.1.0 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.942 + rev: v0.950 hooks: - id: mypy additional_dependencies: [types-all] From af467017c21d4b4e36da87a7b6e933fbc8e48cb6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 4 May 2022 11:49:47 -0700 Subject: [PATCH 635/967] add search term required input to issue template --- .github/ISSUE_TEMPLATE/bug.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml index 6cce5fef7..9ee61d185 100644 --- a/.github/ISSUE_TEMPLATE/bug.yaml +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -9,6 +9,13 @@ body: [pre-commit.ci]: https://pre-commit.ci [pre-commit-ci/issues]: https://github.com/pre-commit-ci/issues + - type: input + id: version + attributes: + label: search tried in the issue tracker + placeholder: ... + validations: + required: true - type: textarea id: freeform attributes: From 96bf685380e3a89f3a03b5d542249f31a4bdfe53 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 4 May 2022 21:22:24 -0400 Subject: [PATCH 636/967] fix non-unique id --- .github/ISSUE_TEMPLATE/bug.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml index 9ee61d185..bfced0f29 100644 --- a/.github/ISSUE_TEMPLATE/bug.yaml +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -10,7 +10,7 @@ body: [pre-commit.ci]: https://pre-commit.ci [pre-commit-ci/issues]: https://github.com/pre-commit-ci/issues - type: input - id: version + id: search attributes: label: search tried in the issue tracker placeholder: ... From cc9d950601cd3eba27e8395a7edcd455262705d9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 5 May 2022 06:54:43 -0700 Subject: [PATCH 637/967] v2.19.0 --- CHANGELOG.md | 24 ++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd31c4b17..1b6d8b654 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,27 @@ +2.19.0 - 2022-05-05 +=================== + +### Features +- Allow multiple outputs from `language: dotnet` hooks. + - #2332 PR by @WallucePinkham. +- Add more information to `healthy()` failure. + - #2348 PR by @asottile. +- Upgrade ruby-build. + - #2342 PR by @jalessio. +- Add `pre-commit validate-config` / `pre-commit validate-manifest` and + deprecate `pre-commit-validate-config` and `pre-commit-validate-manifest`. + - #2362 PR by @asottile. + +### Fixes +- Fix `pre-push` when pushed ref contains spaces. + - #2345 PR by @wwade. + - #2344 issue by @wwade. + +### Updating +- Change `pre-commit-validate-config` / `pre-commit-validate-manifest` to + `pre-commit validate-config` / `pre-commit validate-manifest`. + - #2362 PR by @asottile. + 2.18.1 - 2022-04-02 =================== diff --git a/setup.cfg b/setup.cfg index ca92af3e7..93a485c5a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.18.1 +version = 2.19.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From a54391e96f27b8a75acfa14bac5dc39ea96994da Mon Sep 17 00:00:00 2001 From: Paul Gey Date: Sat, 7 May 2022 20:44:02 +0200 Subject: [PATCH 638/967] Force gem installation into `GEM_HOME` When `--user-install` is set in the gemrc config file, `gem` ignores `GEM_HOME`. `--no-user-install` prevents this behaviour. --- pre_commit/languages/ruby.py | 1 + tests/repository_test.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 6c5cff280..8955dd011 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -138,6 +138,7 @@ def install_environment( ( 'gem', 'install', '--no-document', '--no-format-executable', + '--no-user-install', *prefix.star('.gem'), *additional_dependencies, ), ) diff --git a/tests/repository_test.py b/tests/repository_test.py index 3729ab1d7..11d452ca4 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -332,6 +332,13 @@ def test_run_a_ruby_hook(tempdir_factory, store): ) +def test_run_a_ruby_hook_with_user_install_set(tempdir_factory, store, tmpdir): + gemrc = tmpdir.join('gemrc') + gemrc.write('gem: --user-install\n') + with envcontext((('GEMRC', str(gemrc)),)): + test_run_a_ruby_hook(tempdir_factory, store) + + @xfailif_windows # pragma: win32 no cover def test_run_versioned_ruby_hook(tempdir_factory, store): _test_hook_repo( From 323fd0d18819322359c7479c31f8921f96fac995 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 May 2022 20:28:33 +0000 Subject: [PATCH 639/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.32.0 → v2.32.1](https://github.com/asottile/pyupgrade/compare/v2.32.0...v2.32.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7791f7656..c4cf5b46e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.32.0 + rev: v2.32.1 hooks: - id: pyupgrade args: [--py37-plus] From a84136d070d7f010ad187f808bac2d57bf822506 Mon Sep 17 00:00:00 2001 From: "Gaige B. Paulsen" Date: Sat, 14 May 2022 09:15:03 +0000 Subject: [PATCH 640/967] Switch pty use to fix solaris Use the child instead of parent fd when manipulating pty for color. --- pre_commit/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index 40c53e515..8c296f4d8 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -168,10 +168,10 @@ def __enter__(self) -> Pty: self.r, self.w = openpty() # tty flags normally change \n to \r\n - attrs = termios.tcgetattr(self.r) + attrs = termios.tcgetattr(self.w) assert isinstance(attrs[1], int) attrs[1] &= ~(termios.ONLCR | termios.OPOST) - termios.tcsetattr(self.r, termios.TCSANOW, attrs) + termios.tcsetattr(self.w, termios.TCSANOW, attrs) return self From 34e97023f4b0383abec38d484631daef80d96a6c Mon Sep 17 00:00:00 2001 From: "Gaige B. Paulsen" Date: Sat, 14 May 2022 08:28:08 -0400 Subject: [PATCH 641/967] force default branch name for tests --- testing/fixtures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/fixtures.py b/testing/fixtures.py index ef5a04185..5182a083e 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -38,7 +38,7 @@ def copy_tree_to_path(src_dir, dest_dir): def git_dir(tempdir_factory): path = tempdir_factory.get() - cmd_output('git', 'init', path) + cmd_output('git', '-c', 'init.defaultBranch=master', 'init', path) return path From fb0ccf3546a9cb34ec3692e403270feb6d6033a2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 26 May 2022 09:43:30 -0400 Subject: [PATCH 642/967] correct one slight inaccuracy in language docs --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fa1678ca1..310c17ee8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -72,7 +72,7 @@ to implement. The current implemented languages are at varying levels: - 3rd class - pre-commit requires the user to install both the tool and the language globally (current examples: script, system) -"third class" is usually the easiest to implement first and is perfectly +"second class" is usually the easiest to implement first and is perfectly acceptable. Ideally the language works on the supported platforms for pre-commit (linux, From 50589386af364c9d3f2c46f5f6e328ee9a500c24 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 May 2022 20:32:51 +0000 Subject: [PATCH 643/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.950 → v0.960](https://github.com/pre-commit/mirrors-mypy/compare/v0.950...v0.960) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c4cf5b46e..e50b1b956 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.950 + rev: v0.960 hooks: - id: mypy additional_dependencies: [types-all] From 702ebf402cac8f1914b689ea198bcf3d68422d03 Mon Sep 17 00:00:00 2001 From: Matt Whitaker Date: Fri, 27 May 2022 17:03:21 +0100 Subject: [PATCH 644/967] Expose prepare-commit-msg arguments as environment vars --- pre_commit/commands/hook_impl.py | 18 +++++++++++++++- pre_commit/commands/run.py | 10 +++++++++ pre_commit/main.py | 14 +++++++++++++ testing/util.py | 4 ++++ tests/commands/hook_impl_test.py | 36 ++++++++++++++++++++++++++++++++ tests/commands/run_test.py | 7 ++++++- 6 files changed, 87 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index f315c04de..f5995e9ad 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -76,6 +76,8 @@ def _ns( remote_name: str | None = None, remote_url: str | None = None, commit_msg_filename: str | None = None, + prepare_commit_message_source: str | None = None, + commit_object_name: str | None = None, checkout_type: str | None = None, is_squash_merge: str | None = None, rewrite_command: str | None = None, @@ -90,6 +92,8 @@ def _ns( remote_name=remote_name, remote_url=remote_url, commit_msg_filename=commit_msg_filename, + prepare_commit_message_source=prepare_commit_message_source, + commit_object_name=commit_object_name, all_files=all_files, checkout_type=checkout_type, is_squash_merge=is_squash_merge, @@ -202,8 +206,20 @@ def _run_ns( _check_args_length(hook_type, args) if hook_type == 'pre-push': return _pre_push_ns(color, args, stdin) - elif hook_type in {'commit-msg', 'prepare-commit-msg'}: + elif hook_type in 'commit-msg': return _ns(hook_type, color, commit_msg_filename=args[0]) + elif hook_type == 'prepare-commit-msg' and len(args) == 1: + return _ns(hook_type, color, commit_msg_filename=args[0]) + elif hook_type == 'prepare-commit-msg' and len(args) == 2: + return _ns( + hook_type, color, commit_msg_filename=args[0], + prepare_commit_message_source=args[1], + ) + elif hook_type == 'prepare-commit-msg' and len(args) == 3: + return _ns( + hook_type, color, commit_msg_filename=args[0], + prepare_commit_message_source=args[1], commit_object_name=args[2], + ) elif hook_type in {'post-commit', 'pre-merge-commit', 'pre-commit'}: return _ns(hook_type, color) elif hook_type == 'post-checkout': diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 37f989b57..ad3d766ef 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -361,6 +361,16 @@ def run( ): return 0 + # Expose prepare_commit_message_source / commit_object_name + # as environment variables for the hooks + if args.prepare_commit_message_source: + environ['PRE_COMMIT_COMMIT_MSG_SOURCE'] = ( + args.prepare_commit_message_source + ) + + if args.commit_object_name: + environ['PRE_COMMIT_COMMIT_OBJECT_NAME'] = args.commit_object_name + # Expose from-ref / to-ref as environment variables for hooks to consume if args.from_ref and args.to_ref: # legacy names diff --git a/pre_commit/main.py b/pre_commit/main.py index 6d2814b37..41278ca98 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -107,6 +107,20 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: '--commit-msg-filename', help='Filename to check when running during `commit-msg`', ) + parser.add_argument( + '--prepare-commit-message-source', + help=( + 'Source of the commit message ' + '(typically the second argument to .git/hooks/prepare-commit-msg)' + ), + ) + parser.add_argument( + '--commit-object-name', + help=( + 'Commit object name ' + '(typically the third argument to .git/hooks/prepare-commit-msg)' + ), + ) parser.add_argument( '--remote-name', help='Remote name used by `git push`.', ) diff --git a/testing/util.py b/testing/util.py index 0dd178406..e807f0482 100644 --- a/testing/util.py +++ b/testing/util.py @@ -76,6 +76,8 @@ def run_opts( hook_stage='commit', show_diff_on_failure=False, commit_msg_filename='', + prepare_commit_message_source='', + commit_object_name='', checkout_type='', is_squash_merge='', rewrite_command='', @@ -97,6 +99,8 @@ def run_opts( hook_stage=hook_stage, show_diff_on_failure=show_diff_on_failure, commit_msg_filename=commit_msg_filename, + prepare_commit_message_source=prepare_commit_message_source, + commit_object_name=commit_object_name, checkout_type=checkout_type, is_squash_merge=is_squash_merge, rewrite_command=rewrite_command, diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index 3e20874e3..aa321dabc 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -154,6 +154,42 @@ def test_run_ns_commit_msg(): assert ns.commit_msg_filename == '.git/COMMIT_MSG' +def test_run_ns_prepare_commit_msg_one_arg(): + ns = hook_impl._run_ns( + 'prepare-commit-msg', False, + ('.git/COMMIT_MSG',), b'', + ) + assert ns is not None + assert ns.hook_stage == 'prepare-commit-msg' + assert ns.color is False + assert ns.commit_msg_filename == '.git/COMMIT_MSG' + + +def test_run_ns_prepare_commit_msg_two_arg(): + ns = hook_impl._run_ns( + 'prepare-commit-msg', False, + ('.git/COMMIT_MSG', 'message'), b'', + ) + assert ns is not None + assert ns.hook_stage == 'prepare-commit-msg' + assert ns.color is False + assert ns.commit_msg_filename == '.git/COMMIT_MSG' + assert ns.prepare_commit_message_source == 'message' + + +def test_run_ns_prepare_commit_msg_three_arg(): + ns = hook_impl._run_ns( + 'prepare-commit-msg', False, + ('.git/COMMIT_MSG', 'message', 'HEAD'), b'', + ) + assert ns is not None + assert ns.hook_stage == 'prepare-commit-msg' + assert ns.color is False + assert ns.commit_msg_filename == '.git/COMMIT_MSG' + assert ns.prepare_commit_message_source == 'message' + assert ns.commit_object_name == 'HEAD' + + def test_run_ns_post_commit(): ns = hook_impl._run_ns('post-commit', True, (), b'') assert ns is not None diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 085b063f3..2634c0c5e 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -810,7 +810,12 @@ def test_prepare_commit_msg_hook(cap_out, store, prepare_commit_msg_repo): cap_out, store, prepare_commit_msg_repo, - {'hook_stage': 'prepare-commit-msg', 'commit_msg_filename': filename}, + { + 'hook_stage': 'prepare-commit-msg', + 'commit_msg_filename': filename, + 'prepare_commit_message_source': 'commit', + 'commit_object_name': 'HEAD', + }, expected_outputs=[b'Add "Signed off by:"', b'Passed'], expected_ret=0, stage=False, From efc1d059fa3f0b650810ccebd6aa306c7df5a7f3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Jun 2022 21:57:06 +0000 Subject: [PATCH 645/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.2.0 → v4.3.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.2.0...v4.3.0) - [github.com/asottile/pyupgrade: v2.32.1 → v2.34.0](https://github.com/asottile/pyupgrade/compare/v2.32.1...v2.34.0) - [github.com/pre-commit/mirrors-mypy: v0.960 → v0.961](https://github.com/pre-commit/mirrors-mypy/compare/v0.960...v0.961) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e50b1b956..1bdabc1ce 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 + rev: v4.3.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -25,7 +25,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.32.1 + rev: v2.34.0 hooks: - id: pyupgrade args: [--py37-plus] @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.960 + rev: v0.961 hooks: - id: mypy additional_dependencies: [types-all] From 53643def070f8b106d08727b35fd9b7dc4a7b1a7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 22 Jun 2022 15:56:50 -0700 Subject: [PATCH 646/967] remove unused --config options from commands which don't use it --- pre_commit/main.py | 83 ++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 50 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 41278ca98..b4fa96617 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -181,11 +181,15 @@ def main(argv: Sequence[str] | None = None) -> int: subparsers = parser.add_subparsers(dest='command') - autoupdate_parser = subparsers.add_parser( + def _add_cmd(name: str, *, help: str) -> argparse.ArgumentParser: + parser = subparsers.add_parser(name, help=help) + add_color_option(parser) + return parser + + autoupdate_parser = _add_cmd( 'autoupdate', help="Auto-update pre-commit config to the latest repos' versions.", ) - add_color_option(autoupdate_parser) _add_config_option(autoupdate_parser) autoupdate_parser.add_argument( '--bleeding-edge', action='store_true', @@ -203,34 +207,17 @@ def main(argv: Sequence[str] | None = None) -> int: help='Only update this repository -- may be specified multiple times.', ) - clean_parser = subparsers.add_parser( - 'clean', help='Clean out pre-commit files.', - ) - add_color_option(clean_parser) - _add_config_option(clean_parser) - - hook_impl_parser = subparsers.add_parser('hook-impl') - add_color_option(hook_impl_parser) - _add_config_option(hook_impl_parser) - hook_impl_parser.add_argument('--hook-type') - hook_impl_parser.add_argument('--hook-dir') - hook_impl_parser.add_argument( - '--skip-on-missing-config', action='store_true', - ) - hook_impl_parser.add_argument(dest='rest', nargs=argparse.REMAINDER) + _add_cmd('clean', help='Clean out pre-commit files.') - gc_parser = subparsers.add_parser('gc', help='Clean unused cached repos.') - add_color_option(gc_parser) - _add_config_option(gc_parser) + _add_cmd('gc', help='Clean unused cached repos.') - init_templatedir_parser = subparsers.add_parser( + init_templatedir_parser = _add_cmd( 'init-templatedir', help=( 'Install hook script in a directory intended for use with ' '`git config init.templateDir`.' ), ) - add_color_option(init_templatedir_parser) _add_config_option(init_templatedir_parser) init_templatedir_parser.add_argument( 'directory', help='The directory in which to write the hook script.', @@ -243,10 +230,7 @@ def main(argv: Sequence[str] | None = None) -> int: ) _add_hook_type_option(init_templatedir_parser) - install_parser = subparsers.add_parser( - 'install', help='Install the pre-commit script.', - ) - add_color_option(install_parser) + install_parser = _add_cmd('install', help='Install the pre-commit script.') _add_config_option(install_parser) install_parser.add_argument( '-f', '--overwrite', action='store_true', @@ -268,7 +252,7 @@ def main(argv: Sequence[str] | None = None) -> int: ), ) - install_hooks_parser = subparsers.add_parser( + install_hooks_parser = _add_cmd( 'install-hooks', help=( 'Install hook environments for all environments in the config ' @@ -276,32 +260,24 @@ def main(argv: Sequence[str] | None = None) -> int: 'useful.' ), ) - add_color_option(install_hooks_parser) _add_config_option(install_hooks_parser) - migrate_config_parser = subparsers.add_parser( + migrate_config_parser = _add_cmd( 'migrate-config', help='Migrate list configuration to new map configuration.', ) - add_color_option(migrate_config_parser) _add_config_option(migrate_config_parser) - run_parser = subparsers.add_parser('run', help='Run hooks.') - add_color_option(run_parser) + run_parser = _add_cmd('run', help='Run hooks.') _add_config_option(run_parser) _add_run_options(run_parser) - sample_config_parser = subparsers.add_parser( - 'sample-config', help=f'Produce a sample {C.CONFIG_FILE} file', - ) - add_color_option(sample_config_parser) - _add_config_option(sample_config_parser) + _add_cmd('sample-config', help=f'Produce a sample {C.CONFIG_FILE} file') - try_repo_parser = subparsers.add_parser( + try_repo_parser = _add_cmd( 'try-repo', help='Try the hooks in a repository, useful for developing new hooks.', ) - add_color_option(try_repo_parser) _add_config_option(try_repo_parser) try_repo_parser.add_argument( 'repo', help='Repository to source hooks from.', @@ -315,32 +291,39 @@ def main(argv: Sequence[str] | None = None) -> int: ) _add_run_options(try_repo_parser) - uninstall_parser = subparsers.add_parser( + uninstall_parser = _add_cmd( 'uninstall', help='Uninstall the pre-commit script.', ) - add_color_option(uninstall_parser) _add_config_option(uninstall_parser) _add_hook_type_option(uninstall_parser) - validate_config_parser = subparsers.add_parser( + validate_config_parser = _add_cmd( 'validate-config', help='Validate .pre-commit-config.yaml files', ) - add_color_option(validate_config_parser) - _add_config_option(validate_config_parser) validate_config_parser.add_argument('filenames', nargs='*') - validate_manifest_parser = subparsers.add_parser( + validate_manifest_parser = _add_cmd( 'validate-manifest', help='Validate .pre-commit-hooks.yaml files', ) - add_color_option(validate_manifest_parser) - _add_config_option(validate_manifest_parser) validate_manifest_parser.add_argument('filenames', nargs='*') + # does not use `_add_cmd` because it doesn't use `--color` help = subparsers.add_parser( 'help', help='Show help for a specific command.', ) help.add_argument('help_cmd', nargs='?', help='Command to show help for.') + # not intended for users to call this directly + hook_impl_parser = subparsers.add_parser('hook-impl') + add_color_option(hook_impl_parser) + _add_config_option(hook_impl_parser) + hook_impl_parser.add_argument('--hook-type') + hook_impl_parser.add_argument('--hook-dir') + hook_impl_parser.add_argument( + '--skip-on-missing-config', action='store_true', + ) + hook_impl_parser.add_argument(dest='rest', nargs=argparse.REMAINDER) + # argparse doesn't really provide a way to use a `default` subparser if len(argv) == 0: argv = ['run'] @@ -354,11 +337,11 @@ def main(argv: Sequence[str] | None = None) -> int: with error_handler(), logging_handler(args.color): git.check_for_cygwin_mismatch() + store = Store() + if args.command not in COMMANDS_NO_GIT: _adjust_args_and_chdir(args) - - store = Store() - store.mark_config_used(args.config) + store.mark_config_used(args.config) if args.command == 'autoupdate': return autoupdate( From d8b59300ce44918bcfa070eff06d2f861222c3fa Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 4 Jul 2022 17:57:38 -0400 Subject: [PATCH 647/967] remove imports from TYPE_CHECKING (py37+) Committed via https://github.com/asottile/all-repos --- pre_commit/languages/helpers.py | 5 +---- pre_commit/parse_shebang.py | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 05a71651e..0be08b54b 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -5,9 +5,9 @@ import random import re from typing import Any +from typing import NoReturn from typing import overload from typing import Sequence -from typing import TYPE_CHECKING import pre_commit.constants as C from pre_commit import parse_shebang @@ -16,9 +16,6 @@ from pre_commit.util import cmd_output_b from pre_commit.xargs import xargs -if TYPE_CHECKING: - from typing import NoReturn - FIXED_RANDOM_SEED = 1542676187 SHIMS_RE = re.compile(r'[/\\]shims[/\\]') diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index 3fd3129f1..3ac933c09 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -2,13 +2,10 @@ import os.path from typing import Mapping -from typing import TYPE_CHECKING +from typing import NoReturn from identify.identify import parse_shebang_from_file -if TYPE_CHECKING: - from typing import NoReturn - class ExecutableNotFoundError(OSError): def to_output(self) -> tuple[int, bytes, None]: From 3ebd101eb5e450c9e9d2345ffe72c78838fb2316 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Jul 2022 21:58:49 +0000 Subject: [PATCH 648/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder_python_imports: v3.1.0 → v3.3.0](https://github.com/asottile/reorder_python_imports/compare/v3.1.0...v3.3.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1bdabc1ce..94a35a762 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder_python_imports - rev: v3.1.0 + rev: v3.3.0 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) From 901e831313e897e3b9313f5efbb5ef589b3e279c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Sun, 10 Jul 2022 02:03:56 +0200 Subject: [PATCH 649/967] Tests: Adjust traceback regexes to allow Python 3.11+ ^^^^^^^ Fixes https://github.com/pre-commit/pre-commit/issues/2451 --- tests/error_handler_test.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 31c71d287..47e2afaa4 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -45,9 +45,11 @@ def test_error_handler_fatal_error(mocked_log_and_exit): r'Traceback \(most recent call last\):\n' r' File ".+pre_commit.error_handler.py", line \d+, in error_handler\n' r' yield\n' + r'( \^\^\^\^\^\n)?' r' File ".+tests.error_handler_test.py", line \d+, ' r'in test_error_handler_fatal_error\n' r' raise exc\n' + r'( \^\^\^\^\^\^\^\^\^\n)?' r'(pre_commit\.errors\.)?FatalError: just a test\n', ) pattern.assert_matches(mocked_log_and_exit.call_args[0][3]) @@ -69,9 +71,11 @@ def test_error_handler_uncaught_error(mocked_log_and_exit): r'Traceback \(most recent call last\):\n' r' File ".+pre_commit.error_handler.py", line \d+, in error_handler\n' r' yield\n' + r'( \^\^\^\^\^\n)?' r' File ".+tests.error_handler_test.py", line \d+, ' r'in test_error_handler_uncaught_error\n' r' raise exc\n' + r'( \^\^\^\^\^\^\^\^\^\n)?' r'ValueError: another test\n', ) pattern.assert_matches(mocked_log_and_exit.call_args[0][3]) @@ -93,9 +97,11 @@ def test_error_handler_keyboardinterrupt(mocked_log_and_exit): r'Traceback \(most recent call last\):\n' r' File ".+pre_commit.error_handler.py", line \d+, in error_handler\n' r' yield\n' + r'( \^\^\^\^\^\n)?' r' File ".+tests.error_handler_test.py", line \d+, ' r'in test_error_handler_keyboardinterrupt\n' r' raise exc\n' + r'( \^\^\^\^\^\^\^\^\^\n)?' r'KeyboardInterrupt\n', ) pattern.assert_matches(mocked_log_and_exit.call_args[0][3]) From ebce88c13d09000f6d1c04a6232ad14fe9c5e33d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 10 Jul 2022 14:20:14 -0400 Subject: [PATCH 650/967] remove warnings checks this wasn't all that useful -- and most of it was for checking python 2 things --- tests/conftest.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index b68a1d00c..40c0c0500 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,24 +21,6 @@ from testing.util import git_commit -@pytest.fixture(autouse=True) -def no_warnings(recwarn): - yield - warnings = [] - for warning in recwarn: # pragma: no cover - message = str(warning.message) - # ImportWarning: Not importing directory '...' missing __init__(.py) - if not ( - isinstance(warning.message, ImportWarning) and - message.startswith('Not importing directory ') and - ' missing __init__' in message - ): - warnings.append( - f'{warning.filename}:{warning.lineno} {message}', - ) - assert not warnings - - @pytest.fixture def tempdir_factory(tmpdir): class TmpdirFactory: From 78a2d867feac2c1602a608c1fa4eeecb2f8bb415 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 10 Jul 2022 20:55:02 -0400 Subject: [PATCH 651/967] v2.20.0 --- CHANGELOG.md | 20 ++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b6d8b654..03a7c8006 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +2.20.0 - 2022-07-10 +=================== + +### Features +- Expose `source` and `object-name` (positional args) of `prepare-commit-msg` + hook as `PRE_COMMIT_COMIT_MSG_SOURCE` and `PRE_COMMIT_COMMIT_OBJECT_NAME`. + - #2407 PR by @M-Whitaker. + - #2406 issue by @M-Whitaker. + +### Fixes +- Fix `language: ruby` installs when `--user-install` is set in gemrc. + - #2394 PR by @narpfel. + - #2393 issue by @narpfel. +- Adjust pty setup for solaris. + - #2390 PR by @gaige. + - #2389 issue by @gaige. +- Remove unused `--config` option from `gc`, `sample-config`, + `validate-config`, `validate-manifest` sub-commands. + - #2429 PR by @asottile. + 2.19.0 - 2022-05-05 =================== diff --git a/setup.cfg b/setup.cfg index 93a485c5a..ae214f657 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.19.0 +version = 2.20.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 0cef48edbfac863771aad34e43637e05fd57e56a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Jul 2022 21:25:15 +0000 Subject: [PATCH 652/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder_python_imports: v3.3.0 → v3.8.1](https://github.com/asottile/reorder_python_imports/compare/v3.3.0...v3.8.1) - [github.com/asottile/pyupgrade: v2.34.0 → v2.37.1](https://github.com/asottile/pyupgrade/compare/v2.34.0...v2.37.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 94a35a762..511ffd525 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder_python_imports - rev: v3.3.0 + rev: v3.8.1 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) @@ -25,7 +25,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.34.0 + rev: v2.37.1 hooks: - id: pyupgrade args: [--py37-plus] From db51d3009f5cbeee6aafdc3e7c0cbbd2627a1a78 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 12 Jul 2022 14:08:57 -0400 Subject: [PATCH 653/967] adjust relative --commit-msg-filename if in subdir --- pre_commit/main.py | 8 +++++++ tests/main_test.py | 55 +++++++++++++++++++++++++++------------------- 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index b4fa96617..3915993ff 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -155,6 +155,10 @@ def _adjust_args_and_chdir(args: argparse.Namespace) -> None: args.config = os.path.abspath(args.config) if args.command in {'run', 'try-repo'}: args.files = [os.path.abspath(filename) for filename in args.files] + if args.commit_msg_filename is not None: + args.commit_msg_filename = os.path.abspath( + args.commit_msg_filename, + ) if args.command == 'try-repo' and os.path.exists(args.repo): args.repo = os.path.abspath(args.repo) @@ -164,6 +168,10 @@ def _adjust_args_and_chdir(args: argparse.Namespace) -> None: args.config = os.path.relpath(args.config) if args.command in {'run', 'try-repo'}: args.files = [os.path.relpath(filename) for filename in args.files] + if args.commit_msg_filename is not None: + args.commit_msg_filename = os.path.relpath( + args.commit_msg_filename, + ) if args.command == 'try-repo' and os.path.exists(args.repo): args.repo = os.path.relpath(args.repo) diff --git a/tests/main_test.py b/tests/main_test.py index a7afd6da4..511592622 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -17,6 +17,8 @@ def _args(**kwargs): kwargs.setdefault('command', 'help') kwargs.setdefault('config', C.CONFIG_FILE) + if kwargs['command'] in {'run', 'try-repo'}: + kwargs.setdefault('commit_msg_filename', None) return argparse.Namespace(**kwargs) @@ -35,13 +37,24 @@ def test_adjust_args_and_chdir_noop(in_git_dir): def test_adjust_args_and_chdir_relative_things(in_git_dir): in_git_dir.join('foo/cfg.yaml').ensure() - in_git_dir.join('foo').chdir() - - 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')] + with in_git_dir.join('foo').as_cwd(): + args = _args(command='run', files=['f1', 'f2'], config='cfg.yaml') + main._adjust_args_and_chdir(args) + assert os.getcwd() == in_git_dir + assert args.config == os.path.join('foo', 'cfg.yaml') + assert args.files == [ + os.path.join('foo', 'f1'), + os.path.join('foo', 'f2'), + ] + + +def test_adjust_args_and_chdir_relative_commit_msg(in_git_dir): + in_git_dir.join('foo/cfg.yaml').ensure() + with in_git_dir.join('foo').as_cwd(): + args = _args(command='run', files=[], commit_msg_filename='t.txt') + main._adjust_args_and_chdir(args) + assert os.getcwd() == in_git_dir + assert args.commit_msg_filename == os.path.join('foo', 't.txt') @pytest.mark.skipif(os.name != 'nt', reason='windows feature') @@ -56,24 +69,22 @@ def test_install_on_subst(in_git_dir, store): # pragma: posix no cover def test_adjust_args_and_chdir_non_relative_config(in_git_dir): - in_git_dir.join('foo').ensure_dir().chdir() - - args = _args() - main._adjust_args_and_chdir(args) - assert os.getcwd() == in_git_dir - assert args.config == C.CONFIG_FILE + with in_git_dir.join('foo').ensure_dir().as_cwd(): + args = _args() + main._adjust_args_and_chdir(args) + assert os.getcwd() == in_git_dir + assert args.config == C.CONFIG_FILE def test_adjust_args_try_repo_repo_relative(in_git_dir): - in_git_dir.join('foo').ensure_dir().chdir() - - args = _args(command='try-repo', repo='../foo', files=[]) - assert args.repo is not None - assert os.path.exists(args.repo) - main._adjust_args_and_chdir(args) - assert os.getcwd() == in_git_dir - assert os.path.exists(args.repo) - assert args.repo == 'foo' + with in_git_dir.join('foo').ensure_dir().as_cwd(): + args = _args(command='try-repo', repo='../foo', files=[]) + assert args.repo is not None + assert os.path.exists(args.repo) + main._adjust_args_and_chdir(args) + assert os.getcwd() == in_git_dir + assert os.path.exists(args.repo) + assert args.repo == 'foo' FNS = ( From 7c14405f8bc2e11a80ff7397e86177081fa1ea65 Mon Sep 17 00:00:00 2001 From: Lorenz Walthert Date: Tue, 12 Jul 2022 22:44:31 +0100 Subject: [PATCH 654/967] just bump failing CI From a568f3c818eea994ac22ebe3fb3f4aec7886a26f Mon Sep 17 00:00:00 2001 From: Lorenz Walthert Date: Tue, 12 Jul 2022 22:47:19 +0100 Subject: [PATCH 655/967] enforce binary installs also for dependencies of R packages Similar problem seems to be found in https://github.com/r-lib/devtools/issues/1724 --- pre_commit/languages/r.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 40a001dbf..22b5f2530 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -158,7 +158,7 @@ def _inline_r_setup(code: str) -> str: only be configured via R options once R has started. These are set here. """ with_option = f"""\ - options(install.packages.compile.from.source = "never") + options(install.packages.compile.from.source = "never", pkgType = "binary") {code} """ return with_option From a8bfaab0913901cbb9c7b7be3210a76c14478538 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Jul 2022 21:46:05 +0000 Subject: [PATCH 656/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v1.20.1 → v1.20.2](https://github.com/asottile/setup-cfg-fmt/compare/v1.20.1...v1.20.2) - [github.com/asottile/reorder_python_imports: v3.8.1 → v3.8.2](https://github.com/asottile/reorder_python_imports/compare/v3.8.1...v3.8.2) - [github.com/asottile/pyupgrade: v2.37.1 → v2.37.2](https://github.com/asottile/pyupgrade/compare/v2.37.1...v2.37.2) - [github.com/pre-commit/mirrors-mypy: v0.961 → v0.971](https://github.com/pre-commit/mirrors-mypy/compare/v0.961...v0.971) --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 511ffd525..01c8c8449 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,11 +10,11 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.20.1 + rev: v1.20.2 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder_python_imports - rev: v3.8.1 + rev: v3.8.2 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) @@ -25,7 +25,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.37.1 + rev: v2.37.2 hooks: - id: pyupgrade args: [--py37-plus] @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.961 + rev: v0.971 hooks: - id: mypy additional_dependencies: [types-all] From f4e658fc6e84fd4578833de58e2701fcb1b543ee Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 25 Jul 2022 19:29:09 -0400 Subject: [PATCH 657/967] require a version of virtualenv which is less broken in 3.10+ --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index ae214f657..f86b31439 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,7 +28,7 @@ install_requires = nodeenv>=0.11.1 pyyaml>=5.1 toml - virtualenv>=20.0.8 + virtualenv>=20.10.0 importlib-metadata;python_version<"3.8" python_requires = >=3.7 From 3e920b5ba763cd22caf4b58a7a37be23ae476076 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 22:40:58 +0000 Subject: [PATCH 658/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v1.20.2 → v2.0.0](https://github.com/asottile/setup-cfg-fmt/compare/v1.20.2...v2.0.0) - [github.com/asottile/pyupgrade: v2.37.2 → v2.37.3](https://github.com/asottile/pyupgrade/compare/v2.37.2...v2.37.3) - [github.com/PyCQA/flake8: 4.0.1 → 5.0.2](https://github.com/PyCQA/flake8/compare/4.0.1...5.0.2) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 01c8c8449..cd411e5de 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.20.2 + rev: v2.0.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder_python_imports @@ -25,7 +25,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.37.2 + rev: v2.37.3 hooks: - id: pyupgrade args: [--py37-plus] @@ -34,7 +34,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 + rev: 5.0.2 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy From d4b73c9e889806a9ec50b401876602d8a3517113 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 22:41:21 +0000 Subject: [PATCH 659/967] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- setup.cfg | 4 ---- 1 file changed, 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index f86b31439..afe56848d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,10 +13,6 @@ classifiers = License :: OSI Approved :: MIT License Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy From 317c9e037a185c2c30e1e1220744f0f9bb3fc025 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Aug 2022 22:24:53 +0000 Subject: [PATCH 660/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 5.0.2 → 5.0.4](https://github.com/PyCQA/flake8/compare/5.0.2...5.0.4) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cd411e5de..7fca524c6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 - rev: 5.0.2 + rev: 5.0.4 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy From 587c6b97e77fea48a8bb88dce6aa9a0a6687a4fa Mon Sep 17 00:00:00 2001 From: Mark Korondi Date: Wed, 10 Aug 2022 17:04:05 +0200 Subject: [PATCH 661/967] respect aliases in SKIP when installing environments --- pre_commit/commands/run.py | 6 +++++- tests/commands/run_test.py | 26 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index ad3d766ef..8d11882c0 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -420,7 +420,11 @@ def run( return 1 skips = _get_skips(environ) - to_install = [hook for hook in hooks if hook.id not in skips] + to_install = [ + hook + for hook in hooks + if hook.id not in skips and hook.alias not in skips + ] install_hook_envs(to_install, store) return _run_hooks(config, hooks, skips, args) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 2634c0c5e..3ae3b537c 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -635,6 +635,32 @@ def test_skip_bypasses_installation(cap_out, store, repo_with_passing_hook): assert ret == 0 +def test_skip_alias_bypasses_installation( + cap_out, store, repo_with_passing_hook, +): + config = { + 'repo': 'local', + 'hooks': [ + { + 'id': 'skipme', + 'name': 'skipme-1', + 'alias': 'skipme-1', + 'entry': 'skipme', + 'language': 'python', + 'additional_dependencies': ['/pre-commit-does-not-exist'], + }, + ], + } + add_config_to_repo(repo_with_passing_hook, config) + + ret, printed = _do_run( + cap_out, store, repo_with_passing_hook, + run_opts(all_files=True), + {'SKIP': 'skipme-1'}, + ) + assert ret == 0 + + def test_hook_id_not_in_non_verbose_output( cap_out, store, repo_with_passing_hook, ): From 2405caa352924aa6148b0e4dc52f97291b3ff2b7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 15 Aug 2022 13:46:17 -0400 Subject: [PATCH 662/967] allow `pre-commit run --files ...` against unmerged files --- pre_commit/commands/run.py | 2 +- tests/commands/run_test.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 8d11882c0..37f78f746 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -333,7 +333,7 @@ def run( stash = not args.all_files and not args.files # Check if we have unresolved merge conflict files and fail fast. - if _has_unmerged_paths(): + if stash and _has_unmerged_paths(): logger.error('Unmerged files. Resolve before committing.') return 1 if bool(args.from_ref) != bool(args.to_ref): diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 3ae3b537c..ef865330f 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -536,6 +536,13 @@ def test_merge_conflict(cap_out, store, in_merge_conflict): assert b'Unmerged files. Resolve before committing.' in printed +def test_files_during_merge_conflict(cap_out, store, in_merge_conflict): + opts = run_opts(files=['placeholder']) + ret, printed = _do_run(cap_out, store, in_merge_conflict, opts) + assert ret == 0 + assert b'Bash hook' in printed + + def test_merge_conflict_modified(cap_out, store, in_merge_conflict): # Touch another file so we have unstaged non-conflicting things assert os.path.exists('placeholder') From 7a62bf7be2564ccc4f98456192bfa4e608741411 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Aug 2022 21:15:11 +0000 Subject: [PATCH 663/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-autopep8: v1.6.0 → v1.7.0](https://github.com/pre-commit/mirrors-autopep8/compare/v1.6.0...v1.7.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7fca524c6..e6c63ae8a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.6.0 + rev: v1.7.0 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 From fb608ee1b45607e35a386aa104f434879b78c962 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Sep 2022 22:48:22 +0000 Subject: [PATCH 664/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.37.3 → v2.38.0](https://github.com/asottile/pyupgrade/compare/v2.37.3...v2.38.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e6c63ae8a..af68fee90 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.37.3 + rev: v2.38.0 hooks: - id: pyupgrade args: [--py37-plus] From a95f488e71f6226696926c43140659fbbcad964b Mon Sep 17 00:00:00 2001 From: chrisRedwine Date: Thu, 22 Sep 2022 21:55:26 -0500 Subject: [PATCH 665/967] extend warning if globs are used instead of regex to local hooks --- pre_commit/clientlib.py | 10 +++++++++- tests/clientlib_test.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 9b53e8107..da6ca2be2 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -298,6 +298,14 @@ def check(self, dct: dict[str, Any]) -> None: OptionalSensibleRegexAtHook('files', cfgv.check_string), OptionalSensibleRegexAtHook('exclude', cfgv.check_string), ) +LOCAL_HOOK_DICT = cfgv.Map( + 'Hook', 'id', + + *MANIFEST_HOOK_DICT.items, + + OptionalSensibleRegexAtHook('files', cfgv.check_string), + OptionalSensibleRegexAtHook('exclude', cfgv.check_string), +) CONFIG_REPO_DICT = cfgv.Map( 'Repository', 'repo', @@ -308,7 +316,7 @@ def check(self, dct: dict[str, Any]) -> None: 'repo', cfgv.NotIn(LOCAL, META), ), cfgv.ConditionalRecurse( - 'hooks', cfgv.Array(MANIFEST_HOOK_DICT), + 'hooks', cfgv.Array(LOCAL_HOOK_DICT), 'repo', LOCAL, ), cfgv.ConditionalRecurse( diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index fb36bb55a..9fea7e168 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -15,6 +15,8 @@ from pre_commit.clientlib import MANIFEST_SCHEMA from pre_commit.clientlib import META_HOOK_DICT from pre_commit.clientlib import MigrateShaToRev +from pre_commit.clientlib import OptionalSensibleRegexAtHook +from pre_commit.clientlib import OptionalSensibleRegexAtTop from pre_commit.clientlib import validate_config_main from pre_commit.clientlib import validate_manifest_main from testing.fixtures import sample_local_config @@ -261,6 +263,27 @@ def test_warn_mutable_rev_conditional(): cfgv.validate(config_obj, CONFIG_REPO_DICT) +@pytest.mark.parametrize( + 'validator_cls', + ( + OptionalSensibleRegexAtHook, + OptionalSensibleRegexAtTop, + ), +) +def test_sensible_regex_validators_dont_pass_none(validator_cls): + key = 'files' + with pytest.raises(cfgv.ValidationError) as excinfo: + validator = validator_cls(key, cfgv.check_string) + validator.check({key: None}) + + assert str(excinfo.value) == ( + '\n' + f'==> At key: {key}' + '\n' + '=====> Expected string got NoneType' + ) + + @pytest.mark.parametrize( ('regex', 'warning'), ( @@ -296,6 +319,22 @@ def test_validate_optional_sensible_regex_at_hook(caplog, regex, warning): assert caplog.record_tuples == [('pre_commit', logging.WARNING, warning)] +def test_validate_optional_sensible_regex_at_local_hook(caplog): + config_obj = sample_local_config() + config_obj['hooks'][0]['files'] = r'dir/*.py' + + cfgv.validate(config_obj, CONFIG_REPO_DICT) + + assert caplog.record_tuples == [ + ( + 'pre_commit', + logging.WARNING, + "The 'files' field in hook 'do_not_commit' is a regex, not a glob " + "-- matching '/*' probably isn't what you want here", + ), + ] + + @pytest.mark.parametrize( ('regex', 'warning'), ( From 6d5de9feaf9086f96021d5d63606de54262ca85e Mon Sep 17 00:00:00 2001 From: chrisRedwine Date: Mon, 26 Sep 2022 17:53:14 -0500 Subject: [PATCH 666/967] remove extraneous raw string literal in test --- tests/clientlib_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 9fea7e168..b4c3c4e06 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -321,7 +321,7 @@ def test_validate_optional_sensible_regex_at_hook(caplog, regex, warning): def test_validate_optional_sensible_regex_at_local_hook(caplog): config_obj = sample_local_config() - config_obj['hooks'][0]['files'] = r'dir/*.py' + config_obj['hooks'][0]['files'] = 'dir/*.py' cfgv.validate(config_obj, CONFIG_REPO_DICT) From 404f2dccd57aec10a1fefddb3dba41156c6bc971 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 26 Sep 2022 22:58:40 +0000 Subject: [PATCH 667/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder_python_imports: v3.8.2 → v3.8.3](https://github.com/asottile/reorder_python_imports/compare/v3.8.2...v3.8.3) - [github.com/asottile/add-trailing-comma: v2.2.3 → v2.3.0](https://github.com/asottile/add-trailing-comma/compare/v2.2.3...v2.3.0) - [github.com/asottile/pyupgrade: v2.38.0 → v2.38.2](https://github.com/asottile/pyupgrade/compare/v2.38.0...v2.38.2) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index af68fee90..6ec15b710 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,18 +14,18 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder_python_imports - rev: v3.8.2 + rev: v3.8.3 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) args: [--py37-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma - rev: v2.2.3 + rev: v2.3.0 hooks: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.38.0 + rev: v2.38.2 hooks: - id: pyupgrade args: [--py37-plus] From 495b5991cfa7aeeb35b0dcc44814bffd8d2d04e1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 29 Sep 2022 16:51:19 -0400 Subject: [PATCH 668/967] "yes" is not a valid search --- .github/ISSUE_TEMPLATE/bug.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml index bfced0f29..96cd6c75c 100644 --- a/.github/ISSUE_TEMPLATE/bug.yaml +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -12,7 +12,7 @@ body: - type: input id: search attributes: - label: search tried in the issue tracker + label: search you tried in the issue tracker placeholder: ... validations: required: true From 68be295b759f64e9cc820577c26955f661b81145 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 22:51:41 +0000 Subject: [PATCH 669/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.971 → v0.981](https://github.com/pre-commit/mirrors-mypy/compare/v0.971...v0.981) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6ec15b710..972128026 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.971 + rev: v0.981 hooks: - id: mypy additional_dependencies: [types-all] From 3d4f6db2a01e687659fef8bf4de66d43b39f2d48 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Oct 2022 23:49:52 +0000 Subject: [PATCH 670/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder_python_imports: v3.8.3 → v3.8.4](https://github.com/asottile/reorder_python_imports/compare/v3.8.3...v3.8.4) - [github.com/asottile/pyupgrade: v2.38.2 → v3.1.0](https://github.com/asottile/pyupgrade/compare/v2.38.2...v3.1.0) - [github.com/pre-commit/mirrors-mypy: v0.981 → v0.982](https://github.com/pre-commit/mirrors-mypy/compare/v0.981...v0.982) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 972128026..02d662ccd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder_python_imports - rev: v3.8.3 + rev: v3.8.4 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) @@ -25,7 +25,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.38.2 + rev: v3.1.0 hooks: - id: pyupgrade args: [--py37-plus] @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.981 + rev: v0.982 hooks: - id: mypy additional_dependencies: [types-all] From eb469c756de4282e37da52cc346e70ba9d116e06 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Fri, 30 Sep 2022 11:44:18 +0200 Subject: [PATCH 671/967] Rust as 1st class language --- CONTRIBUTING.md | 4 +- azure-pipelines.yml | 2 + pre_commit/languages/rust.py | 118 +++++++++++++++++++++++++++++------ tests/languages/rust_test.py | 70 +++++++++++++++++++++ tests/repository_test.py | 4 +- 5 files changed, 174 insertions(+), 24 deletions(-) create mode 100644 tests/languages/rust_test.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 310c17ee8..0817681a8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,9 +65,9 @@ to implement. The current implemented languages are at varying levels: - 0th class - pre-commit does not require any dependencies for these languages as they're not actually languages (current examples: fail, pygrep) - 1st class - pre-commit will bootstrap a full interpreter requiring nothing to - be installed globally (current examples: node, ruby) + be installed globally (current examples: node, ruby, rust) - 2nd class - pre-commit requires the user to install the language globally but - will install tools in an isolated fashion (current examples: python, go, rust, + will install tools in an isolated fashion (current examples: python, go, swift, docker). - 3rd class - pre-commit requires the user to install both the tool and the language globally (current examples: script, system) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 454f6f137..34c94f54a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -17,6 +17,8 @@ jobs: parameters: toxenvs: [py37] os: windows + additional_variables: + TEMP: C:\Temp pre_test: - task: UseRubyVersion@0 - powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts" diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 01c373061..5e4ecafa6 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -1,13 +1,20 @@ from __future__ import annotations import contextlib +import functools import os.path +import platform +import shutil +import sys +import tempfile +import urllib.request from typing import Generator from typing import Sequence import toml import pre_commit.constants as C +from pre_commit import parse_shebang from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var @@ -16,24 +23,61 @@ from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b +from pre_commit.util import make_executable +from pre_commit.util import win_exe ENVIRONMENT_DIR = 'rustenv' -get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check -def get_env_patch(target_dir: str) -> PatchesT: +@functools.lru_cache(maxsize=1) +def get_default_version() -> str: + # If rust is already installed, we can save a bunch of setup time by + # using the installed version. + # + # Just detecting the executable does not suffice, because if rustup is + # installed but no toolchain is available, then `cargo` exists but + # cannot be used without installing a toolchain first. + if cmd_output_b('cargo', '--version', retcode=None)[0] == 0: + return 'system' + else: + return C.DEFAULT + + +def _rust_toolchain(language_version: str) -> str: + """Transform the language version into a rust toolchain version.""" + if language_version == C.DEFAULT: + return 'stable' + else: + return language_version + + +def _envdir(prefix: Prefix, version: str) -> str: + directory = helpers.environment_dir(ENVIRONMENT_DIR, version) + return prefix.path(directory) + + +def get_env_patch(target_dir: str, version: str) -> PatchesT: return ( + ('CARGO_HOME', target_dir), ('PATH', (os.path.join(target_dir, 'bin'), os.pathsep, Var('PATH'))), + # Only set RUSTUP_TOOLCHAIN if we don't want use the system's default + # toolchain + *( + (('RUSTUP_TOOLCHAIN', _rust_toolchain(version)),) + if version != 'system' else () + ), ) @contextlib.contextmanager -def in_env(prefix: Prefix) -> Generator[None, None, None]: - target_dir = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), - ) - with envcontext(get_env_patch(target_dir)): +def in_env( + prefix: Prefix, + language_version: str, +) -> Generator[None, None, None]: + with envcontext( + get_env_patch(_envdir(prefix, language_version), language_version), + ): yield @@ -52,15 +96,45 @@ def _add_dependencies( f.truncate() +def install_rust_with_toolchain(toolchain: str) -> None: + with tempfile.TemporaryDirectory() as rustup_dir: + with envcontext((('RUSTUP_HOME', rustup_dir),)): + # acquire `rustup` if not present + if parse_shebang.find_executable('rustup') is None: + # We did not detect rustup and need to download it first. + if sys.platform == 'win32': # pragma: win32 cover + if platform.machine() == 'x86_64': + url = 'https://win.rustup.rs/x86_64' + else: + url = 'https://win.rustup.rs/i686' + else: # pragma: win32 no cover + url = 'https://sh.rustup.rs' + + resp = urllib.request.urlopen(url) + + rustup_init = os.path.join(rustup_dir, win_exe('rustup-init')) + with open(rustup_init, 'wb') as f: + shutil.copyfileobj(resp, f) + make_executable(rustup_init) + + # install rustup into `$CARGO_HOME/bin` + cmd_output_b( + rustup_init, '-y', '--quiet', '--no-modify-path', + '--default-toolchain', 'none', + ) + + cmd_output_b( + 'rustup', 'toolchain', 'install', '--no-self-update', + toolchain, + ) + + def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: - helpers.assert_version_default('rust', version) - directory = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), - ) + directory = _envdir(prefix, version) # There are two cases where we might want to specify more dependencies: # as dependencies for the library being built, and as binary packages @@ -84,17 +158,21 @@ def install_environment( packages_to_install: set[tuple[str, ...]] = {('--path', '.')} for cli_dep in cli_deps: cli_dep = cli_dep[len('cli:'):] - package, _, version = cli_dep.partition(':') - if version != '': - packages_to_install.add((package, '--version', version)) + package, _, crate_version = cli_dep.partition(':') + if crate_version != '': + packages_to_install.add((package, '--version', crate_version)) else: packages_to_install.add((package,)) - for args in packages_to_install: - cmd_output_b( - 'cargo', 'install', '--bins', '--root', directory, *args, - cwd=prefix.prefix_dir, - ) + with in_env(prefix, version): + if version != 'system': + install_rust_with_toolchain(_rust_toolchain(version)) + + for args in packages_to_install: + cmd_output_b( + 'cargo', 'install', '--bins', '--root', directory, *args, + cwd=prefix.prefix_dir, + ) def run_hook( @@ -102,5 +180,5 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: - with in_env(hook.prefix): + with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/tests/languages/rust_test.py b/tests/languages/rust_test.py new file mode 100644 index 000000000..9bf97830a --- /dev/null +++ b/tests/languages/rust_test.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from unittest import mock + +import pytest + +import pre_commit.constants as C +from pre_commit import parse_shebang +from pre_commit.languages import rust +from pre_commit.prefix import Prefix +from pre_commit.util import cmd_output + +ACTUAL_GET_DEFAULT_VERSION = rust.get_default_version.__wrapped__ + + +@pytest.fixture +def cmd_output_b_mck(): + with mock.patch.object(rust, 'cmd_output_b') as mck: + yield mck + + +def test_sets_system_when_rust_is_available(cmd_output_b_mck): + cmd_output_b_mck.return_value = (0, b'', b'') + assert ACTUAL_GET_DEFAULT_VERSION() == 'system' + + +def test_uses_default_when_rust_is_not_available(cmd_output_b_mck): + cmd_output_b_mck.return_value = (127, b'', b'error: not found') + assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT + + +@pytest.mark.parametrize('language_version', (C.DEFAULT, '1.56.0')) +def test_installs_with_bootstrapped_rustup(tmpdir, language_version): + tmpdir.join('src', 'main.rs').ensure().write( + 'fn main() {\n' + ' println!("Hello, world!");\n' + '}\n', + ) + tmpdir.join('Cargo.toml').ensure().write( + '[package]\n' + 'name = "hello_world"\n' + 'version = "0.1.0"\n' + 'edition = "2021"\n', + ) + prefix = Prefix(str(tmpdir)) + + find_executable_exes = [] + + original_find_executable = parse_shebang.find_executable + + def mocked_find_executable(exe: str) -> str | None: + """ + Return `None` the first time `find_executable` is called to ensure + that the bootstrapping code is executed, then just let the function + work as normal. + + Also log the arguments to ensure that everything works as expected. + """ + find_executable_exes.append(exe) + if len(find_executable_exes) == 1: + return None + return original_find_executable(exe) + + with mock.patch.object(parse_shebang, 'find_executable') as find_exe_mck: + find_exe_mck.side_effect = mocked_find_executable + rust.install_environment(prefix, language_version, ()) + assert find_executable_exes == ['rustup', 'rustup', 'cargo'] + + with rust.in_env(prefix, language_version): + assert cmd_output('hello_world')[1] == 'Hello, world!\n' diff --git a/tests/repository_test.py b/tests/repository_test.py index 11d452ca4..0d4cb651b 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -471,7 +471,7 @@ def test_additional_rust_cli_dependencies_installed( hook = _get_hook(config, store, 'rust-hook') binaries = os.listdir( hook.prefix.path( - helpers.environment_dir(rust.ENVIRONMENT_DIR, C.DEFAULT), 'bin', + helpers.environment_dir(rust.ENVIRONMENT_DIR, 'system'), 'bin', ), ) # normalize for windows @@ -490,7 +490,7 @@ def test_additional_rust_lib_dependencies_installed( hook = _get_hook(config, store, 'rust-hook') binaries = os.listdir( hook.prefix.path( - helpers.environment_dir(rust.ENVIRONMENT_DIR, C.DEFAULT), 'bin', + helpers.environment_dir(rust.ENVIRONMENT_DIR, 'system'), 'bin', ), ) # normalize for windows From f9532fb59ab302d8782640c4c1652dc27ab09ec5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 23:10:12 +0000 Subject: [PATCH 672/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v2.0.0 → v2.1.0](https://github.com/asottile/setup-cfg-fmt/compare/v2.0.0...v2.1.0) - [github.com/asottile/reorder_python_imports: v3.8.4 → v3.8.5](https://github.com/asottile/reorder_python_imports/compare/v3.8.4...v3.8.5) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 02d662ccd..08ed35b05 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,11 +10,11 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.0.0 + rev: v2.1.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder_python_imports - rev: v3.8.4 + rev: v3.8.5 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) From bc96b0bcf688f8c5e6494e8bcf67ef72780f4c20 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 22 Oct 2022 09:34:43 -0700 Subject: [PATCH 673/967] fix tests for submodules for CVE-2022-39253 --- pre_commit/git.py | 7 +++---- tox.ini | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 35392b341..40b12f01c 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -3,7 +3,7 @@ import logging import os.path import sys -from typing import MutableMapping +from typing import Mapping from pre_commit.errors import FatalError from pre_commit.util import CalledProcessError @@ -24,9 +24,7 @@ def zsplit(s: str) -> list[str]: return [] -def no_git_env( - _env: MutableMapping[str, str] | None = None, -) -> dict[str, str]: +def no_git_env(_env: Mapping[str, str] | None = None) -> dict[str, str]: # Too many bugs dealing with environment variables and GIT: # https://github.com/pre-commit/pre-commit/issues/300 # In git 2.6.3 (maybe others), git exports GIT_WORK_TREE while running @@ -44,6 +42,7 @@ def no_git_env( 'GIT_EXEC_PATH', 'GIT_SSH', 'GIT_SSH_COMMAND', 'GIT_SSL_CAINFO', 'GIT_SSL_NO_VERIFY', 'GIT_CONFIG_COUNT', 'GIT_HTTP_PROXY_AUTHMETHOD', + 'GIT_ALLOW_PROTOCOL', } } diff --git a/tox.ini b/tox.ini index 7f43e41e4..463b72f35 100644 --- a/tox.ini +++ b/tox.ini @@ -23,5 +23,6 @@ env = GIT_COMMITTER_NAME=test GIT_AUTHOR_EMAIL=test@example.com GIT_COMMITTER_EMAIL=test@example.com + GIT_ALLOW_PROTOCOL=file VIRTUALENV_NO_DOWNLOAD=1 PRE_COMMIT_NO_CONCURRENCY=1 From 8ebb7ae2f574cfda9721865d4399fd273a370dec Mon Sep 17 00:00:00 2001 From: Matt Phillips Date: Thu, 27 Oct 2022 15:32:38 -0400 Subject: [PATCH 674/967] add GIT_ASKPASS as a passthrough env var documented via man gitcredentials, it is used to provide a script/input for git to fetch creds in a no-tty usecase. used among other things by jenkins to pass credentials down to git for authentication. https://github.com/jenkinsci/git-plugin/blob/1e3488a730a169778ba0863dd4edbb1dc29154a1/README.adoc#git-bindings https://github.com/jenkinsci/git-plugin/blob/9429e7d05df3dbb4060ac6ab4da6538bb0eb50ba/src/main/java/jenkins/plugins/git/GitUsernamePasswordBinding.java#L130 --- pre_commit/git.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pre_commit/git.py b/pre_commit/git.py index 40b12f01c..439da7ada 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -43,6 +43,7 @@ def no_git_env(_env: Mapping[str, str] | None = None) -> dict[str, str]: 'GIT_SSL_NO_VERIFY', 'GIT_CONFIG_COUNT', 'GIT_HTTP_PROXY_AUTHMETHOD', 'GIT_ALLOW_PROTOCOL', + 'GIT_ASKPASS', } } From e703982de45ac64492897b25fa4edbdb8da10e62 Mon Sep 17 00:00:00 2001 From: marsha Date: Fri, 28 Oct 2022 20:23:00 -0500 Subject: [PATCH 675/967] Change Rust to install environment with `cargo add` over `toml` --- pre_commit/languages/rust.py | 26 +++++++++++--------------- setup.cfg | 1 - tests/repository_test.py | 2 +- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 5e4ecafa6..0c347b492 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -11,8 +11,6 @@ from typing import Generator from typing import Sequence -import toml - import pre_commit.constants as C from pre_commit import parse_shebang from pre_commit.envcontext import envcontext @@ -82,18 +80,16 @@ def in_env( def _add_dependencies( - cargo_toml_path: str, + prefix: Prefix, additional_dependencies: set[str], ) -> None: - with open(cargo_toml_path, 'r+') as f: - cargo_toml = toml.load(f) - cargo_toml.setdefault('dependencies', {}) - for dep in additional_dependencies: - name, _, spec = dep.partition(':') - cargo_toml['dependencies'][name] = spec or '*' - f.seek(0) - toml.dump(cargo_toml, f) - f.truncate() + crates = [] + for dep in additional_dependencies: + name, _, spec = dep.partition(':') + crate = f'{name}@{spec or "*"}' + crates.append(crate) + + helpers.run_setup_cmd(prefix, ('cargo', 'add', *crates)) def install_rust_with_toolchain(toolchain: str) -> None: @@ -151,9 +147,6 @@ def install_environment( } lib_deps = set(additional_dependencies) - cli_deps - if len(lib_deps) > 0: - _add_dependencies(prefix.path('Cargo.toml'), lib_deps) - with clean_path_on_failure(directory): packages_to_install: set[tuple[str, ...]] = {('--path', '.')} for cli_dep in cli_deps: @@ -168,6 +161,9 @@ def install_environment( if version != 'system': install_rust_with_toolchain(_rust_toolchain(version)) + if len(lib_deps) > 0: + _add_dependencies(prefix, lib_deps) + for args in packages_to_install: cmd_output_b( 'cargo', 'install', '--bins', '--root', directory, *args, diff --git a/setup.cfg b/setup.cfg index afe56848d..ab95cc042 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,7 +23,6 @@ install_requires = identify>=1.0.0 nodeenv>=0.11.1 pyyaml>=5.1 - toml virtualenv>=20.10.0 importlib-metadata;python_version<"3.8" python_requires = >=3.7 diff --git a/tests/repository_test.py b/tests/repository_test.py index 0d4cb651b..252c126c0 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -485,7 +485,7 @@ def test_additional_rust_lib_dependencies_installed( path = make_repo(tempdir_factory, 'rust_hooks_repo') config = make_config_from_repo(path) # A small rust package with no dependencies. - deps = ['shellharden:3.1.0'] + deps = ['shellharden:3.1.0', 'git-version'] config['hooks'][0]['additional_dependencies'] = deps hook = _get_hook(config, store, 'rust-hook') binaries = os.listdir( From 84b38f7b89fa22bea8bc70b03e664cd6cda9db84 Mon Sep 17 00:00:00 2001 From: marsha Date: Sun, 30 Oct 2022 14:47:42 -0500 Subject: [PATCH 676/967] Change `cmd_output_b`s `retcode` arg to a boolean `check` --- pre_commit/commands/run.py | 4 ++-- pre_commit/error_handler.py | 2 +- pre_commit/git.py | 4 ++-- pre_commit/languages/node.py | 2 +- pre_commit/languages/rust.py | 2 +- pre_commit/staged_files_only.py | 2 +- pre_commit/util.py | 11 ++++++----- pre_commit/xargs.py | 2 +- tests/commands/init_templatedir_test.py | 2 +- tests/commands/install_uninstall_test.py | 6 +++--- tests/commands/run_test.py | 4 ++-- tests/conftest.py | 2 +- tests/error_handler_test.py | 3 ++- tests/git_test.py | 2 +- tests/util_test.py | 6 +++--- 15 files changed, 28 insertions(+), 26 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 37f78f746..429e04c60 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -263,7 +263,7 @@ def _all_filenames(args: argparse.Namespace) -> Collection[str]: def _get_diff() -> bytes: _, out, _ = cmd_output_b( - 'git', 'diff', '--no-ext-diff', '--ignore-submodules', retcode=None, + 'git', 'diff', '--no-ext-diff', '--ignore-submodules', check=False, ) return out @@ -318,7 +318,7 @@ def _has_unmerged_paths() -> bool: def _has_unstaged_config(config_file: str) -> bool: retcode, _, _ = cmd_output_b( 'git', 'diff', '--no-ext-diff', '--exit-code', config_file, - retcode=None, + check=False, ) # be explicit, other git errors don't mean it has an unstaged config. return retcode == 1 diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 992f5cdc0..d740ee3e4 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -25,7 +25,7 @@ def _log_and_exit( error_msg = f'{msg}: {type(exc).__name__}: '.encode() + force_bytes(exc) output.write_line_b(error_msg) - _, git_version_b, _ = cmd_output_b('git', '--version', retcode=None) + _, git_version_b, _ = cmd_output_b('git', '--version', check=False) git_version = git_version_b.decode(errors='backslashreplace').rstrip() storedir = Store().directory diff --git a/pre_commit/git.py b/pre_commit/git.py index 439da7ada..37ed3a718 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -187,11 +187,11 @@ def head_rev(remote: str) -> str: def has_diff(*args: str, repo: str = '.') -> bool: cmd = ('git', 'diff', '--quiet', '--no-ext-diff', *args) - return cmd_output_b(*cmd, cwd=repo, retcode=None)[0] == 1 + return cmd_output_b(*cmd, cwd=repo, check=False)[0] == 1 def has_core_hookpaths_set() -> bool: - _, out, _ = cmd_output_b('git', 'config', 'core.hooksPath', retcode=None) + _, out, _ = cmd_output_b('git', 'config', 'core.hooksPath', check=False) return bool(out.strip()) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 39f300065..37a5b63f1 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -75,7 +75,7 @@ def in_env( def health_check(prefix: Prefix, language_version: str) -> str | None: with in_env(prefix, language_version): - retcode, _, _ = cmd_output_b('node', '--version', retcode=None) + retcode, _, _ = cmd_output_b('node', '--version', check=False) if retcode != 0: # pragma: win32 no cover return f'`node --version` returned {retcode}' else: diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 0c347b492..ef603bc00 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -36,7 +36,7 @@ def get_default_version() -> str: # Just detecting the executable does not suffice, because if rustup is # installed but no toolchain is available, then `cargo` exists but # cannot be used without installing a toolchain first. - if cmd_output_b('cargo', '--version', retcode=None)[0] == 0: + if cmd_output_b('cargo', '--version', check=False)[0] == 0: return 'system' else: return C.DEFAULT diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 83d8a03e1..172fb20b1 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -52,7 +52,7 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: retcode, diff_stdout_binary, _ = cmd_output_b( 'git', 'diff-index', '--ignore-submodules', '--binary', '--exit-code', '--no-color', '--no-ext-diff', tree, '--', - retcode=None, + check=False, ) if retcode and diff_stdout_binary.strip(): patch_filename = f'patch{int(time.time())}-{os.getpid()}' diff --git a/pre_commit/util.py b/pre_commit/util.py index 8c296f4d8..a935c2d85 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -124,7 +124,7 @@ def _oserror_to_output(e: OSError) -> tuple[int, bytes, None]: def cmd_output_b( *cmd: str, - retcode: int | None = 0, + check: bool = True, **kwargs: Any, ) -> tuple[int, bytes, bytes | None]: _setdefault_kwargs(kwargs) @@ -142,8 +142,9 @@ def cmd_output_b( stdout_b, stderr_b = proc.communicate() returncode = proc.returncode - if retcode is not None and retcode != returncode: - raise CalledProcessError(returncode, cmd, retcode, stdout_b, stderr_b) + SUCCESS = 0 + if check and returncode != SUCCESS: + raise CalledProcessError(returncode, cmd, SUCCESS, stdout_b, stderr_b) return returncode, stdout_b, stderr_b @@ -196,10 +197,10 @@ def __exit__( def cmd_output_p( *cmd: str, - retcode: int | None = 0, + check: bool = True, **kwargs: Any, ) -> tuple[int, bytes, bytes | None]: - assert retcode is None + assert check is False assert kwargs['stderr'] == subprocess.STDOUT, kwargs['stderr'] _setdefault_kwargs(kwargs) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index f2b3421a0..e3af90efd 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -154,7 +154,7 @@ def run_cmd_partition( run_cmd: tuple[str, ...], ) -> tuple[int, bytes, bytes | None]: return cmd_fn( - *run_cmd, retcode=None, stderr=subprocess.STDOUT, **kwargs, + *run_cmd, check=False, stderr=subprocess.STDOUT, **kwargs, ) threads = min(len(partitions), target_concurrency) diff --git a/tests/commands/init_templatedir_test.py b/tests/commands/init_templatedir_test.py index 64bfc8b4e..28f29b77c 100644 --- a/tests/commands/init_templatedir_test.py +++ b/tests/commands/init_templatedir_test.py @@ -135,7 +135,7 @@ def test_init_templatedir_skip_on_missing_config( retcode, output = git_commit( fn=cmd_output_mocked_pre_commit_home, tempdir_factory=tempdir_factory, - retcode=None, + check=False, ) assert retcode == commit_retcode diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index ae668ac9f..379c03a4f 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -126,7 +126,7 @@ def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): cmd_output('git', 'add', touch_file) return git_commit( fn=cmd_output_mocked_pre_commit_home, - retcode=None, + check=False, tempdir_factory=tempdir_factory, **kwargs, ) @@ -286,7 +286,7 @@ def test_environment_not_sourced(tempdir_factory, store): 'GIT_AUTHOR_EMAIL': os.environ['GIT_AUTHOR_EMAIL'], 'GIT_COMMITTER_EMAIL': os.environ['GIT_COMMITTER_EMAIL'], }, - retcode=None, + check=False, ) assert ret == 1 assert out == ( @@ -551,7 +551,7 @@ def _get_push_output(tempdir_factory, remote='origin', opts=()): return cmd_output_mocked_pre_commit_home( 'git', 'push', remote, 'HEAD:new_branch', *opts, tempdir_factory=tempdir_factory, - retcode=None, + check=False, )[:2] diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index ef865330f..03d741e06 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -718,7 +718,7 @@ def test_non_ascii_hook_id(repo_with_passing_hook, tempdir_factory): with cwd(repo_with_passing_hook): _, stdout, _ = cmd_output_mocked_pre_commit_home( sys.executable, '-m', 'pre_commit.main', 'run', '☃', - retcode=None, tempdir_factory=tempdir_factory, + check=False, tempdir_factory=tempdir_factory, ) assert 'UnicodeDecodeError' not in stdout # Doesn't actually happen, but a reasonable assertion @@ -737,7 +737,7 @@ def test_stdout_write_bug_py26(repo_with_failing_hook, store, tempdir_factory): _, out = git_commit( fn=cmd_output_mocked_pre_commit_home, tempdir_factory=tempdir_factory, - retcode=None, + check=False, ) assert 'UnicodeEncodeError' not in out # Doesn't actually happen, but a reasonable assertion diff --git a/tests/conftest.py b/tests/conftest.py index 40c0c0500..30761715b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -68,7 +68,7 @@ def _make_conflict(): bar_only_file.write('bar') cmd_output('git', 'add', 'bar_only_file') git_commit(msg=_make_conflict.__name__) - cmd_output('git', 'merge', 'foo', retcode=None) + cmd_output('git', 'merge', 'foo', check=False) @pytest.fixture diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 47e2afaa4..068149e3f 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -183,10 +183,11 @@ def test_error_handler_no_tty(tempdir_factory): 'from pre_commit.error_handler import error_handler\n' 'with error_handler():\n' ' raise ValueError("\\u2603")\n', - retcode=3, + check=False, tempdir_factory=tempdir_factory, pre_commit_home=pre_commit_home, ) + assert ret == 3 log_file = os.path.join(pre_commit_home, 'pre-commit.log') out_lines = out.splitlines() assert out_lines[-2] == 'An unexpected error has occurred: ValueError: ☃' diff --git a/tests/git_test.py b/tests/git_test.py index b9f524a14..93f5a1c6e 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -104,7 +104,7 @@ def test_is_in_merge_conflict_submodule(in_conflicting_submodule): def test_cherry_pick_conflict(in_merge_conflict): cmd_output('git', 'merge', '--abort') foo_ref = cmd_output('git', 'rev-parse', 'foo')[1].strip() - cmd_output('git', 'cherry-pick', foo_ref, retcode=None) + cmd_output('git', 'cherry-pick', foo_ref, check=False) assert git.is_in_merge_conflict() is False diff --git a/tests/util_test.py b/tests/util_test.py index 6b00f9fc6..bc8f585f8 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -83,14 +83,14 @@ def test_tmpdir(): def test_cmd_output_exe_not_found(): - ret, out, _ = cmd_output('dne', retcode=None) + ret, out, _ = cmd_output('dne', check=False) assert ret == 1 assert out == 'Executable `dne` not found' @pytest.mark.parametrize('fn', (cmd_output_b, cmd_output_p)) def test_cmd_output_exe_not_found_bytes(fn): - ret, out, _ = fn('dne', retcode=None, stderr=subprocess.STDOUT) + ret, out, _ = fn('dne', check=False, stderr=subprocess.STDOUT) assert ret == 1 assert out == b'Executable `dne` not found' @@ -101,7 +101,7 @@ def test_cmd_output_no_shebang(tmpdir, fn): make_executable(f) # previously this raised `OSError` -- the output is platform specific - ret, out, _ = fn(str(f), retcode=None, stderr=subprocess.STDOUT) + ret, out, _ = fn(str(f), check=False, stderr=subprocess.STDOUT) assert ret == 1 assert isinstance(out, bytes) assert out.endswith(b'\n') From 42102a1bfd96f70ae817f90ac2e7e1b07eae933d Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Sun, 30 Oct 2022 15:18:13 -0500 Subject: [PATCH 677/967] Remove `expected_returncode` from `CalledProcessError` --- pre_commit/util.py | 10 +++------- tests/error_handler_test.py | 2 +- tests/languages/docker_test.py | 2 +- tests/store_test.py | 2 +- tests/util_test.py | 6 ++---- 5 files changed, 8 insertions(+), 14 deletions(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index a935c2d85..b85076883 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -83,14 +83,12 @@ def __init__( self, returncode: int, cmd: tuple[str, ...], - expected_returncode: int, stdout: bytes, stderr: bytes | None, ) -> None: - super().__init__(returncode, cmd, expected_returncode, stdout, stderr) + super().__init__(returncode, cmd, stdout, stderr) self.returncode = returncode self.cmd = cmd - self.expected_returncode = expected_returncode self.stdout = stdout self.stderr = stderr @@ -104,7 +102,6 @@ def _indent_or_none(part: bytes | None) -> bytes: return b''.join(( f'command: {self.cmd!r}\n'.encode(), f'return code: {self.returncode}\n'.encode(), - f'expected return code: {self.expected_returncode}\n'.encode(), b'stdout:', _indent_or_none(self.stdout), b'\n', b'stderr:', _indent_or_none(self.stderr), )) @@ -142,9 +139,8 @@ def cmd_output_b( stdout_b, stderr_b = proc.communicate() returncode = proc.returncode - SUCCESS = 0 - if check and returncode != SUCCESS: - raise CalledProcessError(returncode, cmd, SUCCESS, stdout_b, stderr_b) + if check and returncode: + raise CalledProcessError(returncode, cmd, stdout_b, stderr_b) return returncode, stdout_b, stderr_b diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 068149e3f..a79d9c1a9 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -162,7 +162,7 @@ def test_error_handler_non_ascii_exception(mock_store_dir): def test_error_handler_non_utf8_exception(mock_store_dir): with pytest.raises(SystemExit): with error_handler.error_handler(): - raise CalledProcessError(1, ('exe',), 0, b'error: \xa0\xe1', b'') + raise CalledProcessError(1, ('exe',), b'error: \xa0\xe1', b'') def test_error_handler_non_stringable_exception(mock_store_dir): diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 58387611f..5f7c85e71 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -178,6 +178,6 @@ def test_get_docker_path_in_docker_windows(in_docker): def test_get_docker_path_in_docker_docker_in_docker(in_docker): # won't be able to discover "self" container in true docker-in-docker - err = CalledProcessError(1, (), 0, b'', b'') + err = CalledProcessError(1, (), b'', b'') with mock.patch.object(docker, 'cmd_output_b', side_effect=err): assert docker._get_docker_path('/project') == '/project' diff --git a/tests/store_test.py b/tests/store_test.py index ff671a83d..818776623 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -127,7 +127,7 @@ def test_clone_shallow_failure_fallback_to_complete( # Force shallow clone failure def fake_shallow_clone(self, *args, **kwargs): - raise CalledProcessError(1, (), 0, b'', None) + raise CalledProcessError(1, (), b'', None) store._shallow_clone = fake_shallow_clone ret = store.clone(path, rev) diff --git a/tests/util_test.py b/tests/util_test.py index bc8f585f8..b3f18b4cf 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -18,11 +18,10 @@ def test_CalledProcessError_str(): - error = CalledProcessError(1, ('exe',), 0, b'output', b'errors') + error = CalledProcessError(1, ('exe',), b'output', b'errors') assert str(error) == ( "command: ('exe',)\n" 'return code: 1\n' - 'expected return code: 0\n' 'stdout:\n' ' output\n' 'stderr:\n' @@ -31,11 +30,10 @@ def test_CalledProcessError_str(): def test_CalledProcessError_str_nooutput(): - error = CalledProcessError(1, ('exe',), 0, b'', b'') + error = CalledProcessError(1, ('exe',), b'', b'') assert str(error) == ( "command: ('exe',)\n" 'return code: 1\n' - 'expected return code: 0\n' 'stdout: (none)\n' 'stderr: (none)' ) From 3b3cf8c1f1536e69a9cc536f0f2f41d3847b6237 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 23:27:34 +0000 Subject: [PATCH 678/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v2.1.0 → v2.2.0](https://github.com/asottile/setup-cfg-fmt/compare/v2.1.0...v2.2.0) - [github.com/asottile/reorder_python_imports: v3.8.5 → v3.9.0](https://github.com/asottile/reorder_python_imports/compare/v3.8.5...v3.9.0) - [github.com/asottile/pyupgrade: v3.1.0 → v3.2.0](https://github.com/asottile/pyupgrade/compare/v3.1.0...v3.2.0) - [github.com/pre-commit/mirrors-autopep8: v1.7.0 → v2.0.0](https://github.com/pre-commit/mirrors-autopep8/compare/v1.7.0...v2.0.0) --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 08ed35b05..1a4b56776 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,11 +10,11 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.1.0 + rev: v2.2.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder_python_imports - rev: v3.8.5 + rev: v3.9.0 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) @@ -25,12 +25,12 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v3.1.0 + rev: v3.2.0 hooks: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.7.0 + rev: v2.0.0 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 From 4bca29ee2cb6631a4d9420e443674c6905a81705 Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Wed, 2 Nov 2022 17:00:32 -0500 Subject: [PATCH 679/967] Change `intent_to_add_files` from using `git status` to `git diff` --- pre_commit/git.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 37ed3a718..f84eb06bd 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -150,18 +150,10 @@ def get_staged_files(cwd: str | None = None) -> list[str]: def intent_to_add_files() -> list[str]: _, stdout, _ = cmd_output( - 'git', 'status', '--ignore-submodules', '--porcelain', '-z', + 'git', 'diff', '--ignore-submodules', '--diff-filter=A', + '--name-only', '-z', ) - parts = list(reversed(zsplit(stdout))) - intent_to_add = [] - while parts: - line = parts.pop() - status, filename = line[:3], line[3:] - if status[0] in {'C', 'R'}: # renames / moves have an additional arg - parts.pop() - if status[1] == 'A': - intent_to_add.append(filename) - return intent_to_add + return zsplit(stdout) def get_all_files() -> list[str]: From 97ad4f89ecc9a7462baf78b29d1ee47f121304a1 Mon Sep 17 00:00:00 2001 From: mishaschwartz Date: Tue, 1 Nov 2022 13:09:29 -0400 Subject: [PATCH 680/967] ruby: update ruby-build to 20220710 to ensure that the correct openssl version is used --- pre_commit/resources/ruby-build.tar.gz | Bin 72569 -> 74032 bytes testing/make-archives | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz index 8edb3caa3c27f1ccff9f0e0dc36e9863845c4efa..35419f63aebe33b6710851aef70937cd9ef163ba 100644 GIT binary patch literal 74032 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

Q#vRC5;yit2Sd`xrHX$e_C?P2bQUVfE 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

@UO6IsN-Ld{vdH%#= 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

FB_o9 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%0Da>5o|PaFgyRW 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 diff --git a/testing/make-archives b/testing/make-archives index 04b42dd9d..704101f51 100755 --- a/testing/make-archives +++ b/testing/make-archives @@ -17,7 +17,7 @@ from typing import Sequence REPOS = ( ('rbenv', 'https://github.com/rbenv/rbenv', '38e1fbb'), - ('ruby-build', 'https://github.com/rbenv/ruby-build', '2004fd7'), + ('ruby-build', 'https://github.com/rbenv/ruby-build', '98c0337'), ( 'ruby-download', 'https://github.com/garnieretienne/rvm-download', From 4399c2dbc6b7571489a524eedecfba9cd53d93a8 Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Thu, 10 Nov 2022 20:09:56 -0600 Subject: [PATCH 681/967] Add `--no-ext-diff` to `git diff` call --- pre_commit/git.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index f84eb06bd..a76118f0b 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -150,8 +150,8 @@ def get_staged_files(cwd: str | None = None) -> list[str]: def intent_to_add_files() -> list[str]: _, stdout, _ = cmd_output( - 'git', 'diff', '--ignore-submodules', '--diff-filter=A', - '--name-only', '-z', + 'git', 'diff', '--no-ext-diff', '--ignore-submodules', + '--diff-filter=A', '--name-only', '-z', ) return zsplit(stdout) From 371b4fc1fd33267c0ca0fe6962eee4fa76c4305d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Nov 2022 23:36:47 +0000 Subject: [PATCH 682/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.2.0 → v3.2.2](https://github.com/asottile/pyupgrade/compare/v3.2.0...v3.2.2) - [github.com/pre-commit/mirrors-mypy: v0.982 → v0.990](https://github.com/pre-commit/mirrors-mypy/compare/v0.982...v0.990) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1a4b56776..ad5fecb41 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v3.2.0 + rev: v3.2.2 hooks: - id: pyupgrade args: [--py37-plus] @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.982 + rev: v0.990 hooks: - id: mypy additional_dependencies: [types-all] From 318296d8c5427430510620bc443393a290f6db92 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 16 Nov 2022 19:19:49 -0500 Subject: [PATCH 683/967] remove no_implicit_optional this is the default in mypy 0.990 Committed via https://github.com/asottile/all-repos --- setup.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index ab95cc042..dd0f9c9a4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -56,7 +56,6 @@ check_untyped_defs = true disallow_any_generics = true disallow_incomplete_defs = true disallow_untyped_defs = true -no_implicit_optional = true warn_redundant_casts = true warn_unused_ignores = true From 391d05e2f30916677919bcea2128414e807d0ff0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 21 Nov 2022 23:11:57 +0000 Subject: [PATCH 684/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.990 → v0.991](https://github.com/pre-commit/mirrors-mypy/compare/v0.990...v0.991) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ad5fecb41..44172896c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.990 + rev: v0.991 hooks: - id: mypy additional_dependencies: [types-all] From 50c217964b0f00e38d67cac858b597501a86e22b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 27 Nov 2022 16:30:58 -0500 Subject: [PATCH 685/967] remove obsolete comment --- pre_commit/commands/sample_config.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pre_commit/commands/sample_config.py b/pre_commit/commands/sample_config.py index 82a1617f8..ce22f65e4 100644 --- a/pre_commit/commands/sample_config.py +++ b/pre_commit/commands/sample_config.py @@ -1,7 +1,3 @@ -# TODO: maybe `git ls-remote git://github.com/pre-commit/pre-commit-hooks` to -# determine the latest revision? This adds ~200ms from my tests (and is -# significantly faster than https:// or http://). For now, periodically -# manually updating the revision is fine. from __future__ import annotations SAMPLE_CONFIG = '''\ # See https://pre-commit.com for more information From df7bcf78c3b1c056150b5476a5a58b6bd0d8c8d5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 29 Nov 2022 01:09:52 +0000 Subject: [PATCH 686/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.3.0 → v4.4.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.3.0...v4.4.0) - [github.com/PyCQA/flake8: 5.0.4 → 6.0.0](https://github.com/PyCQA/flake8/compare/5.0.4...6.0.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 44172896c..ee1492c6f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -34,7 +34,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 - rev: 5.0.4 + rev: 6.0.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy From 5becd50974ea39caea357be2d62c940120ead91a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 5 Dec 2022 23:55:06 -0500 Subject: [PATCH 687/967] update swift for jammy --- testing/get-swift.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/testing/get-swift.sh b/testing/get-swift.sh index b77e18c0e..3e7808241 100755 --- a/testing/get-swift.sh +++ b/testing/get-swift.sh @@ -3,9 +3,9 @@ set -euo pipefail . /etc/lsb-release -if [ "$DISTRIB_CODENAME" = "focal" ]; then - SWIFT_URL='https://download.swift.org/swift-5.6.1-release/ubuntu2004/swift-5.6.1-RELEASE/swift-5.6.1-RELEASE-ubuntu20.04.tar.gz' - SWIFT_HASH='2b4f22d4a8b59fe8e050f0b7f020f8d8f12553cbda56709b2340a4a3bb90cfea' +if [ "$DISTRIB_CODENAME" = "jammy" ]; then + SWIFT_URL='https://download.swift.org/swift-5.7.1-release/ubuntu2204/swift-5.7.1-RELEASE/swift-5.7.1-RELEASE-ubuntu22.04.tar.gz' + SWIFT_HASH='7f60291f5088d3e77b0c2364beaabd29616ee7b37260b7b06bdbeb891a7fe161' else echo "unknown dist: ${DISTRIB_CODENAME}" 1>&2 exit 1 From 6c524f7a554f03a247eb76ac48da78b8eef27389 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 23 Nov 2022 14:45:08 -0500 Subject: [PATCH 688/967] fix rust platform detection on windows --- pre_commit/languages/rust.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index ef603bc00..204f2aa79 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -3,7 +3,6 @@ import contextlib import functools import os.path -import platform import shutil import sys import tempfile @@ -99,10 +98,7 @@ def install_rust_with_toolchain(toolchain: str) -> None: if parse_shebang.find_executable('rustup') is None: # We did not detect rustup and need to download it first. if sys.platform == 'win32': # pragma: win32 cover - if platform.machine() == 'x86_64': - url = 'https://win.rustup.rs/x86_64' - else: - url = 'https://win.rustup.rs/i686' + url = 'https://win.rustup.rs/x86_64' else: # pragma: win32 no cover url = 'https://sh.rustup.rs' From 46c64efd9d8da13e20292f26429d228e4f76958f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 6 Dec 2022 15:00:06 -0500 Subject: [PATCH 689/967] update .net framework target --- testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj | 2 +- testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj | 2 +- .../dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj | 2 +- .../dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj b/testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj index 4f714d339..861ced6d9 100644 --- a/testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj +++ b/testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj @@ -2,7 +2,7 @@ Exe - net5.0 + net6 true proj1 diff --git a/testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj b/testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj index da451f7cc..dfce2cad1 100644 --- a/testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj +++ b/testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj @@ -2,7 +2,7 @@ Exe - net5.0 + net6 true proj2 diff --git a/testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj b/testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj index d2e556ac0..fa9879b0d 100644 --- a/testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj +++ b/testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj @@ -1,7 +1,7 @@ Exe - netcoreapp3.1 + net6 true testeroni ./nupkg diff --git a/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj b/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj index e37296480..a4e2d0058 100644 --- a/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj +++ b/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj @@ -1,7 +1,7 @@ Exe - netcoreapp3.1 + net6 true testeroni ./nupkg From 0b45ecc8a40d7d980b43a7f37d4f18b8e390aa8d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 6 Dec 2022 15:35:10 -0500 Subject: [PATCH 690/967] remove python 2.x cross version tests --- CONTRIBUTING.md | 1 - .../python3_hooks_repo/.pre-commit-hooks.yaml | 1 + tests/repository_test.py | 34 ++----------------- 3 files changed, 4 insertions(+), 32 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0817681a8..a9bcb79ed 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,6 @@ - The complete test suite depends on having at least the following installed (possibly not a complete list) - git (Version 2.24.0 or above is required to run pre-merge-commit tests) - - python2 (Required by a test which checks different python versions) - python3 (Required by a test which checks different python versions) - tox (or virtualenv) - ruby + gem diff --git a/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml index 964cf8363..2c2370092 100644 --- a/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml +++ b/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml @@ -2,4 +2,5 @@ name: Python 3 Hook entry: python3-hook language: python + language_version: python3 files: \.py$ diff --git a/tests/repository_test.py b/tests/repository_test.py index 252c126c0..64d8a1701 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -173,23 +173,6 @@ def test_python_venv(tempdir_factory, store): ) -@xfailif_windows # pragma: win32 no cover # no python 2 in GHA -def test_switch_language_versions_doesnt_clobber(tempdir_factory, store): - # We're using the python3 repo because it prints the python version - path = make_repo(tempdir_factory, 'python3_hooks_repo') - - def run_on_version(version, expected_output): - config = make_config_from_repo(path) - config['hooks'][0]['language_version'] = version - hook = _get_hook(config, store, 'python3-hook') - ret, out = _hook_run(hook, [], color=False) - assert ret == 0 - assert _norm_out(out) == expected_output - - run_on_version('python2', b'2\n[]\nHello World\n') - run_on_version('python3', b'3\n[]\nHello World\n') - - def test_versioned_python_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'python3_hooks_repo', @@ -883,7 +866,7 @@ def test_tags_on_repositories(in_tmpdir, tempdir_factory, store): @pytest.fixture def local_python_config(): # Make a "local" hooks repo that just installs our other hooks repo - repo_path = get_resource_path('python3_hooks_repo') + repo_path = get_resource_path('python_hooks_repo') manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) hooks = [ dict(hook, additional_dependencies=[repo_path]) for hook in manifest @@ -892,23 +875,12 @@ def local_python_config(): def test_local_python_repo(store, local_python_config): - hook = _get_hook(local_python_config, store, 'python3-hook') - # language_version should have been adjusted to the interpreter version - assert hook.language_version != C.DEFAULT - ret, out = _hook_run(hook, ('filename',), color=False) - assert ret == 0 - assert _norm_out(out) == b"3\n['filename']\nHello World\n" - - -@xfailif_windows # pragma: win32 no cover # no python2 in GHA -def test_local_python_repo_python2(store, local_python_config): - local_python_config['hooks'][0]['language_version'] = 'python2' - hook = _get_hook(local_python_config, store, 'python3-hook') + hook = _get_hook(local_python_config, store, 'foo') # language_version should have been adjusted to the interpreter version assert hook.language_version != C.DEFAULT ret, out = _hook_run(hook, ('filename',), color=False) assert ret == 0 - assert _norm_out(out) == b"2\n['filename']\nHello World\n" + assert _norm_out(out) == b"['filename']\nHello World\n" def test_default_language_version(store, local_python_config): From 92c70766fd2db34fa025c2d8c7dad25f6cb0b17e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 6 Dec 2022 16:47:56 -0500 Subject: [PATCH 691/967] fix rust coverage on windows it's a complete mystery why this isn't needed on other platforms, the branch is legitimately uncovered there --- tests/languages/rust_test.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/languages/rust_test.py b/tests/languages/rust_test.py index 9bf97830a..f011e7199 100644 --- a/tests/languages/rust_test.py +++ b/tests/languages/rust_test.py @@ -68,3 +68,23 @@ def mocked_find_executable(exe: str) -> str | None: with rust.in_env(prefix, language_version): assert cmd_output('hello_world')[1] == 'Hello, world!\n' + + +def test_installs_with_existing_rustup(tmpdir): + tmpdir.join('src', 'main.rs').ensure().write( + 'fn main() {\n' + ' println!("Hello, world!");\n' + '}\n', + ) + tmpdir.join('Cargo.toml').ensure().write( + '[package]\n' + 'name = "hello_world"\n' + 'version = "0.1.0"\n' + 'edition = "2021"\n', + ) + prefix = Prefix(str(tmpdir)) + + assert parse_shebang.find_executable('rustup') is not None + rust.install_environment(prefix, '1.56.0', ()) + with rust.in_env(prefix, '1.56.0'): + assert cmd_output('hello_world')[1] == 'Hello, world!\n' From b92fe017559d93061c8c259db389ecd8e4f39b00 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 10 Dec 2022 23:33:20 -0500 Subject: [PATCH 692/967] force the `-p` branch to run for language: python under test --- tests/repository_test.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/repository_test.py b/tests/repository_test.py index 64d8a1701..6d50cb84c 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -173,13 +173,20 @@ def test_python_venv(tempdir_factory, store): ) -def test_versioned_python_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'python3_hooks_repo', - 'python3-hook', - [os.devnull], - f'3\n[{os.devnull!r}]\nHello World\n'.encode(), - ) +def test_language_versioned_python_hook(tempdir_factory, store): + # we patch this force virtualenv executing with `-p` since we can't + # reliably have multiple pythons available in CI + with mock.patch.object( + python, + '_sys_executable_matches', + return_value=False, + ): + _test_hook_repo( + tempdir_factory, store, 'python3_hooks_repo', + 'python3-hook', + [os.devnull], + f'3\n[{os.devnull!r}]\nHello World\n'.encode(), + ) @skipif_cant_run_coursier # pragma: win32 no cover From 8cc3a6d8aa45b9984972bb0f73b9b6dcb3227273 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 10 Dec 2022 23:45:04 -0500 Subject: [PATCH 693/967] passenv is stupid anyway --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 463b72f35..e06be115b 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist = py37,py38,pypy3,pre-commit [testenv] deps = -rrequirements-dev.txt -passenv = APPDATA HOME LOCALAPPDATA PROGRAMFILES RUSTUP_HOME +passenv = * commands = coverage erase coverage run -m pytest {posargs:tests} From b00c31cf9e917b5ac62e093e9cca6a59d3677992 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 12 Dec 2022 12:22:39 -0500 Subject: [PATCH 694/967] use a newer version of ruby which builds cleanly --- .../ruby_versioned_hooks_repo/.pre-commit-hooks.yaml | 2 +- tests/languages/ruby_test.py | 4 ++-- tests/repository_test.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml index 63e1dd4c6..364d47d8f 100644 --- a/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml +++ b/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml @@ -2,5 +2,5 @@ name: Ruby Hook entry: ruby_hook language: ruby - language_version: 2.5.1 + language_version: 3.1.0 files: \.rb$ diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index dc55456e8..29f3c802e 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -71,10 +71,10 @@ def test_install_ruby_default(fake_gem_prefix): @xfailif_windows # pragma: win32 no cover def test_install_ruby_with_version(fake_gem_prefix): - ruby.install_environment(fake_gem_prefix, '2.7.2', ()) + ruby.install_environment(fake_gem_prefix, '3.1.0', ()) # Should be able to activate and use rbenv install - with ruby.in_env(fake_gem_prefix, '2.7.2'): + with ruby.in_env(fake_gem_prefix, '3.1.0'): cmd_output('rbenv', 'install', '--help') diff --git a/tests/repository_test.py b/tests/repository_test.py index 6d50cb84c..8705d8860 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -335,7 +335,7 @@ def test_run_versioned_ruby_hook(tempdir_factory, store): tempdir_factory, store, 'ruby_versioned_hooks_repo', 'ruby_hook', [os.devnull], - b'2.5.1\nHello world from a ruby hook\n', + b'3.1.0\nHello world from a ruby hook\n', ) @@ -357,7 +357,7 @@ def test_run_ruby_hook_with_disable_shared_gems( tempdir_factory, store, 'ruby_versioned_hooks_repo', 'ruby_hook', [os.devnull], - b'2.5.1\nHello world from a ruby hook\n', + b'3.1.0\nHello world from a ruby hook\n', ) From 6ab7fc25d56872824252ff7357c2d7a754bfcb1e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Dec 2022 23:51:53 +0000 Subject: [PATCH 695/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.2.2 → v3.3.0](https://github.com/asottile/pyupgrade/compare/v3.2.2...v3.3.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ee1492c6f..3aecdc13a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v3.2.2 + rev: v3.3.0 hooks: - id: pyupgrade args: [--py37-plus] From 52948f610c458cad95f60fbc7b0337780a98c9f0 Mon Sep 17 00:00:00 2001 From: Lorenz Walthert Date: Tue, 6 Dec 2022 12:23:47 +0100 Subject: [PATCH 696/967] When R executable is an explicit path, we need to appene `.exe` on Windows --- pre_commit/languages/r.py | 3 ++- tests/languages/r_test.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 22b5f2530..d281102b2 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -15,6 +15,7 @@ from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b +from pre_commit.util import win_exe ENVIRONMENT_DIR = 'renv' RSCRIPT_OPTS = ('--no-save', '--no-restore', '--no-site-file', '--no-environ') @@ -63,7 +64,7 @@ def _rscript_exec() -> str: if r_home is None: return 'Rscript' else: - return os.path.join(r_home, 'bin', 'Rscript') + return os.path.join(r_home, 'bin', win_exe('Rscript')) def _entry_validate(entry: Sequence[str]) -> None: diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py index 5bc63b27c..c52d5acd3 100644 --- a/tests/languages/r_test.py +++ b/tests/languages/r_test.py @@ -6,6 +6,7 @@ from pre_commit import envcontext from pre_commit.languages import r +from pre_commit.util import win_exe from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo from tests.repository_test import _get_hook_no_install @@ -133,7 +134,7 @@ def test_r_parsing_file_local(tempdir_factory, store): def test_rscript_exec_relative_to_r_home(): - expected = os.path.join('r_home_dir', 'bin', 'Rscript') + expected = os.path.join('r_home_dir', 'bin', win_exe('Rscript')) with envcontext.envcontext((('R_HOME', 'r_home_dir'),)): assert r._rscript_exec() == expected From a179808bfeb63094b2127402da8cb4eeccb5be2d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 13 Dec 2022 00:40:31 +0000 Subject: [PATCH 697/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/add-trailing-comma: v2.3.0 → v2.4.0](https://github.com/asottile/add-trailing-comma/compare/v2.3.0...v2.4.0) - [github.com/asottile/pyupgrade: v3.3.0 → v3.3.1](https://github.com/asottile/pyupgrade/compare/v3.3.0...v3.3.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3aecdc13a..c3b261bd1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,12 +20,12 @@ repos: exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) args: [--py37-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma - rev: v2.3.0 + rev: v2.4.0 hooks: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v3.3.0 + rev: v3.3.1 hooks: - id: pyupgrade args: [--py37-plus] From 94b617890624fc760a91289019ef4608e1a386fe Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 Dec 2022 00:30:07 +0000 Subject: [PATCH 698/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-autopep8: v2.0.0 → v2.0.1](https://github.com/pre-commit/mirrors-autopep8/compare/v2.0.0...v2.0.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c3b261bd1..7e58bdd81 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v2.0.0 + rev: v2.0.1 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 From e904628830490042426e411512bfb4d519de891b Mon Sep 17 00:00:00 2001 From: Ruairidh MacLeod Date: Tue, 6 Dec 2022 12:04:19 +0000 Subject: [PATCH 699/967] fix dotnet hooks with prefixes --- pre_commit/languages/dotnet.py | 32 ++++++++++++++++--- .../.gitignore | 3 ++ .../.pre-commit-hooks.yaml | 5 +++ .../Program.cs | 12 +++++++ .../dotnet_hooks_csproj_prefix_repo.csproj | 9 ++++++ tests/repository_test.py | 1 + 6 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 testing/resources/dotnet_hooks_csproj_prefix_repo/.gitignore create mode 100644 testing/resources/dotnet_hooks_csproj_prefix_repo/.pre-commit-hooks.yaml create mode 100644 testing/resources/dotnet_hooks_csproj_prefix_repo/Program.cs create mode 100644 testing/resources/dotnet_hooks_csproj_prefix_repo/dotnet_hooks_csproj_prefix_repo.csproj diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index 3983c6f0c..9ebda2f73 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -2,6 +2,9 @@ import contextlib import os.path +import re +import xml.etree.ElementTree +import zipfile from typing import Generator from typing import Sequence @@ -57,10 +60,29 @@ def install_environment( ), ) - # Determine tool from the packaged file ..nupkg - build_outputs = os.listdir(os.path.join(prefix.prefix_dir, build_dir)) - for output in build_outputs: - tool_name = output.split('.')[0] + nupkg_dir = prefix.path(build_dir) + nupkgs = [x for x in os.listdir(nupkg_dir) if x.endswith('.nupkg')] + + if not nupkgs: + raise AssertionError('could not find any build outputs to install') + + for nupkg in nupkgs: + with zipfile.ZipFile(os.path.join(nupkg_dir, nupkg)) as f: + nuspec, = (x for x in f.namelist() if x.endswith('.nuspec')) + with f.open(nuspec) as spec: + tree = xml.etree.ElementTree.parse(spec) + + namespace = re.match(r'{.*}', tree.getroot().tag) + if not namespace: + raise AssertionError('could not parse namespace from nuspec') + + tool_id_element = tree.find(f'.//{namespace[0]}id') + if tool_id_element is None: + raise AssertionError('expected to find an "id" element') + + tool_id = tool_id_element.text + if not tool_id: + raise AssertionError('"id" element missing tool name') # Install to bin dir helpers.run_setup_cmd( @@ -69,7 +91,7 @@ def install_environment( 'dotnet', 'tool', 'install', '--tool-path', os.path.join(envdir, BIN_DIR), '--add-source', build_dir, - tool_name, + tool_id, ), ) diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/.gitignore b/testing/resources/dotnet_hooks_csproj_prefix_repo/.gitignore new file mode 100644 index 000000000..edcd28f4a --- /dev/null +++ b/testing/resources/dotnet_hooks_csproj_prefix_repo/.gitignore @@ -0,0 +1,3 @@ +bin/ +obj/ +nupkg/ diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_csproj_prefix_repo/.pre-commit-hooks.yaml new file mode 100644 index 000000000..6626627d7 --- /dev/null +++ b/testing/resources/dotnet_hooks_csproj_prefix_repo/.pre-commit-hooks.yaml @@ -0,0 +1,5 @@ +- id: dotnet-example-hook + name: dotnet example hook + entry: testeroni.tool + language: dotnet + files: '' diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/Program.cs b/testing/resources/dotnet_hooks_csproj_prefix_repo/Program.cs new file mode 100644 index 000000000..1456e8ef2 --- /dev/null +++ b/testing/resources/dotnet_hooks_csproj_prefix_repo/Program.cs @@ -0,0 +1,12 @@ +using System; + +namespace dotnet_hooks_repo +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello from dotnet!"); + } + } +} diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/dotnet_hooks_csproj_prefix_repo.csproj b/testing/resources/dotnet_hooks_csproj_prefix_repo/dotnet_hooks_csproj_prefix_repo.csproj new file mode 100644 index 000000000..754b76006 --- /dev/null +++ b/testing/resources/dotnet_hooks_csproj_prefix_repo/dotnet_hooks_csproj_prefix_repo.csproj @@ -0,0 +1,9 @@ + + + Exe + net7.0 + true + testeroni.tool + ./nupkg + + diff --git a/tests/repository_test.py b/tests/repository_test.py index 8705d8860..c3936bf2f 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1031,6 +1031,7 @@ def test_local_perl_additional_dependencies(store): 'dotnet_hooks_csproj_repo', 'dotnet_hooks_sln_repo', 'dotnet_hooks_combo_repo', + 'dotnet_hooks_csproj_prefix_repo', ), ) def test_dotnet_hook(tempdir_factory, store, repo): From c38e0c7ba8e8a98338a3ed492f83b896337244e6 Mon Sep 17 00:00:00 2001 From: Ruairidh MacLeod Date: Tue, 6 Dec 2022 12:45:44 +0000 Subject: [PATCH 700/967] dotnet: ignore nuget source during tool install --- pre_commit/languages/dotnet.py | 37 +++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index 9ebda2f73..e26b45c3a 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -3,6 +3,7 @@ import contextlib import os.path import re +import tempfile import xml.etree.ElementTree import zipfile from typing import Generator @@ -38,6 +39,22 @@ def in_env(prefix: Prefix) -> Generator[None, None, None]: yield +@contextlib.contextmanager +def _nuget_config_no_sources() -> Generator[str, None, None]: + with tempfile.TemporaryDirectory() as tmpdir: + nuget_config = os.path.join(tmpdir, 'nuget.config') + with open(nuget_config, 'w') as f: + f.write( + '' + '' + ' ' + ' ' + ' ' + '', + ) + yield nuget_config + + def install_environment( prefix: Prefix, version: str, @@ -85,15 +102,17 @@ def install_environment( raise AssertionError('"id" element missing tool name') # Install to bin dir - helpers.run_setup_cmd( - prefix, - ( - 'dotnet', 'tool', 'install', - '--tool-path', os.path.join(envdir, BIN_DIR), - '--add-source', build_dir, - tool_id, - ), - ) + with _nuget_config_no_sources() as nuget_config: + helpers.run_setup_cmd( + prefix, + ( + 'dotnet', 'tool', 'install', + '--configfile', nuget_config, + '--tool-path', os.path.join(envdir, BIN_DIR), + '--add-source', build_dir, + tool_id, + ), + ) # Clean the git dir, ignoring the environment dir clean_cmd = ('git', 'clean', '-ffxd', '-e', f'{ENVIRONMENT_DIR}-*') From 40c5bdad65da4015af0e5ffe88227053109aecf3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 25 Dec 2022 17:52:05 -0500 Subject: [PATCH 701/967] v2.21.0 --- CHANGELOG.md | 40 ++++++++++++++++++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03a7c8006..cd0de5f73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,43 @@ +2.21.0 - 2022-12-25 +=================== + +### Features +- Require new-enough virtualenv to prevent 3.10 breakage + - #2467 PR by @asottile. +- Respect aliases with `SKIP` for environment install. + - #2480 PR by @kmARC. + - #2478 issue by @kmARC. +- Allow `pre-commit run --files` against unmerged paths. + - #2484 PR by @asottile. +- Also apply regex warnings to `repo: local` hooks. + - #2524 PR by @chrisRedwine. + - #2521 issue by @asottile. +- `rust` is now a "first class" language -- supporting `language_version` and + installation when not present. + - #2534 PR by @Holzhaus. +- `r` now uses more-reliable binary installation. + - #2460 PR by @lorenzwalthert. +- `GIT_ALLOW_PROTOCOL` is now passed through for git operations. + - #2555 PR by @asottile. +- `GIT_ASKPASS` is now passed through for git operations. + - #2564 PR by @mattp-. +- Remove `toml` dependency by using `cargo add` directly. + - #2568 PR by @m-rsha. +- Support `dotnet` hooks which have dotted prefixes. + - #2641 PR by @rkm. + - #2629 issue by @rkm. + +### Fixes +- Properly adjust `--commit-msg-filename` if run from a sub directory. + - #2459 PR by @asottile. +- Simplify `--intent-to-add` detection by using `git diff`. + - #2580 PR by @m-rsha. +- Fix `R.exe` selection on windows. + - #2605 PR by @lorenzwalthert. + - #2599 issue by @SInginc. +- Skip default `nuget` source when installing `dotnet` packages. + - #2642 PR by @rkm. + 2.20.0 - 2022-07-10 =================== diff --git a/setup.cfg b/setup.cfg index dd0f9c9a4..a89889217 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.20.0 +version = 2.21.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 524a23607217e115c2f1f51b3bbb869f186040ad Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 23 Dec 2022 18:37:33 -0500 Subject: [PATCH 702/967] drop python<3.8 --- .pre-commit-config.yaml | 4 ++-- azure-pipelines.yml | 6 +++--- pre_commit/constants.py | 9 ++------- setup.cfg | 3 +-- tox.ini | 2 +- 5 files changed, 9 insertions(+), 15 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7e58bdd81..b7d7f1f0d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) - args: [--py37-plus, --add-import, 'from __future__ import annotations'] + args: [--py38-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma rev: v2.4.0 hooks: @@ -28,7 +28,7 @@ repos: rev: v3.3.1 hooks: - id: pyupgrade - args: [--py37-plus] + args: [--py38-plus] - repo: https://github.com/pre-commit/mirrors-autopep8 rev: v2.0.1 hooks: diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 34c94f54a..911ef32d5 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -15,7 +15,7 @@ resources: jobs: - template: job--python-tox.yml@asottile parameters: - toxenvs: [py37] + toxenvs: [py38] os: windows additional_variables: TEMP: C:\Temp @@ -34,7 +34,7 @@ jobs: displayName: install R - template: job--python-tox.yml@asottile parameters: - toxenvs: [py37] + toxenvs: [py38] os: linux name_postfix: _latest_git pre_test: @@ -52,7 +52,7 @@ jobs: displayName: install R - template: job--python-tox.yml@asottile parameters: - toxenvs: [py37, py38, py39] + toxenvs: [py38, py39, py310] os: linux pre_test: - task: UseRubyVersion@0 diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 5bc4ae98b..8fc5e55db 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -1,11 +1,6 @@ from __future__ import annotations -import sys - -if sys.version_info >= (3, 8): # pragma: >=3.8 cover - import importlib.metadata as importlib_metadata -else: # pragma: <3.8 cover - import importlib_metadata +import importlib.metadata CONFIG_FILE = '.pre-commit-config.yaml' MANIFEST_FILE = '.pre-commit-hooks.yaml' @@ -15,7 +10,7 @@ # Bump when modifying `empty_template` LOCAL_REPO_VERSION = '1' -VERSION = importlib_metadata.version('pre_commit') +VERSION = importlib.metadata.version('pre_commit') # `manual` is not invoked by any installed git hook. See #719 STAGES = ( diff --git a/setup.cfg b/setup.cfg index a89889217..1d28a41c8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,8 +24,7 @@ install_requires = nodeenv>=0.11.1 pyyaml>=5.1 virtualenv>=20.10.0 - importlib-metadata;python_version<"3.8" -python_requires = >=3.7 +python_requires = >=3.8 [options.packages.find] exclude = diff --git a/tox.ini b/tox.ini index e06be115b..a44f93d48 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37,py38,pypy3,pre-commit +envlist = py,pypy3,pre-commit [testenv] deps = -rrequirements-dev.txt From 0024484f5b6b0b8a811c0bed4773c1fd28a98503 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 27 Dec 2022 11:15:45 -0500 Subject: [PATCH 703/967] remove support for top-level list format --- pre_commit/clientlib.py | 15 +-- tests/clientlib_test.py | 8 -- tests/commands/install_uninstall_test.py | 122 ++++++++++++----------- 3 files changed, 66 insertions(+), 79 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index da6ca2be2..df8d2e2d0 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -391,23 +391,10 @@ class InvalidConfigError(FatalError): pass -def ordered_load_normalize_legacy_config(contents: str) -> dict[str, Any]: - data = yaml_load(contents) - if isinstance(data, list): - logger.warning( - 'normalizing pre-commit configuration to a top-level map. ' - 'support for top level list will be removed in a future version. ' - 'run: `pre-commit migrate-config` to automatically fix this.', - ) - return {'repos': data} - else: - return data - - load_config = functools.partial( cfgv.load_from_filename, schema=CONFIG_SCHEMA, - load_strategy=ordered_load_normalize_legacy_config, + load_strategy=yaml_load, exc_tp=InvalidConfigError, ) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index b4c3c4e06..23d9352f0 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -120,14 +120,6 @@ def test_validate_config_main_ok(): assert not validate_config_main(('.pre-commit-config.yaml',)) -def test_validate_config_old_list_format_ok(tmpdir, cap_out): - f = tmpdir.join('cfg.yaml') - f.write('- {repo: meta, hooks: [{id: identity}]}') - assert not validate_config_main((f.strpath,)) - msg = '[WARNING] normalizing pre-commit configuration to a top-level map' - assert msg in cap_out.get() - - def test_validate_warn_on_unknown_keys_at_repo_level(tmpdir, caplog): f = tmpdir.join('cfg.yaml') f.write( diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 379c03a4f..e3943773f 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -739,20 +739,22 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): def test_post_commit_integration(tempdir_factory, store): path = git_dir(tempdir_factory) - config = [ - { - 'repo': 'local', - 'hooks': [{ - 'id': 'post-commit', - 'name': 'Post commit', - 'entry': 'touch post-commit.tmp', - 'language': 'system', - 'always_run': True, - 'verbose': True, - 'stages': ['post-commit'], - }], - }, - ] + config = { + 'repos': [ + { + 'repo': 'local', + 'hooks': [{ + 'id': 'post-commit', + 'name': 'Post commit', + 'entry': 'touch post-commit.tmp', + 'language': 'system', + 'always_run': True, + 'verbose': True, + 'stages': ['post-commit'], + }], + }, + ], + } write_config(path, config) with cwd(path): _get_commit_output(tempdir_factory) @@ -765,20 +767,22 @@ def test_post_commit_integration(tempdir_factory, store): def test_post_merge_integration(tempdir_factory, store): path = git_dir(tempdir_factory) - config = [ - { - 'repo': 'local', - 'hooks': [{ - 'id': 'post-merge', - 'name': 'Post merge', - 'entry': 'touch post-merge.tmp', - 'language': 'system', - 'always_run': True, - 'verbose': True, - 'stages': ['post-merge'], - }], - }, - ] + config = { + 'repos': [ + { + 'repo': 'local', + 'hooks': [{ + 'id': 'post-merge', + 'name': 'Post merge', + 'entry': 'touch post-merge.tmp', + 'language': 'system', + 'always_run': True, + 'verbose': True, + 'stages': ['post-merge'], + }], + }, + ], + } write_config(path, config) with cwd(path): # create a simple diamond of commits for a non-trivial merge @@ -807,20 +811,22 @@ def test_post_merge_integration(tempdir_factory, store): def test_post_rewrite_integration(tempdir_factory, store): path = git_dir(tempdir_factory) - config = [ - { - 'repo': 'local', - 'hooks': [{ - 'id': 'post-rewrite', - 'name': 'Post rewrite', - 'entry': 'touch post-rewrite.tmp', - 'language': 'system', - 'always_run': True, - 'verbose': True, - 'stages': ['post-rewrite'], - }], - }, - ] + config = { + 'repos': [ + { + 'repo': 'local', + 'hooks': [{ + 'id': 'post-rewrite', + 'name': 'Post rewrite', + 'entry': 'touch post-rewrite.tmp', + 'language': 'system', + 'always_run': True, + 'verbose': True, + 'stages': ['post-rewrite'], + }], + }, + ], + } write_config(path, config) with cwd(path): open('init', 'a').close() @@ -836,21 +842,23 @@ def test_post_rewrite_integration(tempdir_factory, store): def test_post_checkout_integration(tempdir_factory, store): path = git_dir(tempdir_factory) - config = [ - { - 'repo': 'local', - 'hooks': [{ - 'id': 'post-checkout', - 'name': 'Post checkout', - 'entry': 'bash -c "echo ${PRE_COMMIT_TO_REF}"', - 'language': 'system', - 'always_run': True, - 'verbose': True, - 'stages': ['post-checkout'], - }], - }, - {'repo': 'meta', 'hooks': [{'id': 'identity'}]}, - ] + config = { + 'repos': [ + { + 'repo': 'local', + 'hooks': [{ + 'id': 'post-checkout', + 'name': 'Post checkout', + 'entry': 'bash -c "echo ${PRE_COMMIT_TO_REF}"', + 'language': 'system', + 'always_run': True, + 'verbose': True, + 'stages': ['post-checkout'], + }], + }, + {'repo': 'meta', 'hooks': [{'id': 'identity'}]}, + ], + } write_config(path, config) with cwd(path): cmd_output('git', 'add', '.') From ff3150d58a2ca9441ed6f0a9f3dcc2623301124a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 27 Dec 2022 11:29:00 -0500 Subject: [PATCH 704/967] remove support for sha to specify rev --- pre_commit/clientlib.py | 44 +++++------------------------------------ tests/clientlib_test.py | 43 ---------------------------------------- 2 files changed, 5 insertions(+), 82 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index da6ca2be2..bbb4915fc 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -114,8 +114,7 @@ def validate_manifest_main(argv: Sequence[str] | None = None) -> int: META = 'meta' -# should inherit from cfgv.Conditional if sha support is dropped -class WarnMutableRev(cfgv.ConditionalOptional): +class WarnMutableRev(cfgv.Conditional): def check(self, dct: dict[str, Any]) -> None: super().check(dct) @@ -171,36 +170,6 @@ def check(self, dct: dict[str, Any]) -> None: ) -class MigrateShaToRev: - key = 'rev' - - @staticmethod - def _cond(key: str) -> cfgv.Conditional: - return cfgv.Conditional( - key, cfgv.check_string, - condition_key='repo', - condition_value=cfgv.NotIn(LOCAL, META), - ensure_absent=True, - ) - - def check(self, dct: dict[str, Any]) -> None: - if dct.get('repo') in {LOCAL, META}: - self._cond('rev').check(dct) - self._cond('sha').check(dct) - elif 'sha' in dct and 'rev' in dct: - raise cfgv.ValidationError('Cannot specify both sha and rev') - elif 'sha' in dct: - self._cond('sha').check(dct) - else: - self._cond('rev').check(dct) - - def apply_default(self, dct: dict[str, Any]) -> None: - if 'sha' in dct: - dct['rev'] = dct.pop('sha') - - remove_default = cfgv.Required.remove_default - - def _entry(modname: str) -> str: """the hook `entry` is passed through `shlex.split()` by the command runner, so to prevent issues with spaces and backslashes (on Windows) @@ -324,14 +293,11 @@ def check(self, dct: dict[str, Any]) -> None: 'repo', META, ), - MigrateShaToRev(), WarnMutableRev( - 'rev', - cfgv.check_string, - '', - 'repo', - cfgv.NotIn(LOCAL, META), - True, + 'rev', cfgv.check_string, + condition_key='repo', + condition_value=cfgv.NotIn(LOCAL, META), + ensure_absent=True, ), cfgv.WarnAdditionalKeys(('repo', 'rev', 'hooks'), warn_unknown_keys_repo), ) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index b4c3c4e06..985860465 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -14,7 +14,6 @@ from pre_commit.clientlib import DEFAULT_LANGUAGE_VERSION from pre_commit.clientlib import MANIFEST_SCHEMA from pre_commit.clientlib import META_HOOK_DICT -from pre_commit.clientlib import MigrateShaToRev from pre_commit.clientlib import OptionalSensibleRegexAtHook from pre_commit.clientlib import OptionalSensibleRegexAtTop from pre_commit.clientlib import validate_config_main @@ -425,48 +424,6 @@ def test_valid_manifests(manifest_obj, expected): assert ret is expected -@pytest.mark.parametrize( - 'dct', - ( - {'repo': 'local'}, {'repo': 'meta'}, - {'repo': 'wat', 'sha': 'wat'}, {'repo': 'wat', 'rev': 'wat'}, - ), -) -def test_migrate_sha_to_rev_ok(dct): - MigrateShaToRev().check(dct) - - -def test_migrate_sha_to_rev_dont_specify_both(): - with pytest.raises(cfgv.ValidationError) as excinfo: - MigrateShaToRev().check({'repo': 'a', 'sha': 'b', 'rev': 'c'}) - msg, = excinfo.value.args - assert msg == 'Cannot specify both sha and rev' - - -@pytest.mark.parametrize( - 'dct', - ( - {'repo': 'a'}, - {'repo': 'meta', 'sha': 'a'}, {'repo': 'meta', 'rev': 'a'}, - ), -) -def test_migrate_sha_to_rev_conditional_check_failures(dct): - with pytest.raises(cfgv.ValidationError): - MigrateShaToRev().check(dct) - - -def test_migrate_to_sha_apply_default(): - dct = {'repo': 'a', 'sha': 'b'} - MigrateShaToRev().apply_default(dct) - assert dct == {'repo': 'a', 'rev': 'b'} - - -def test_migrate_to_sha_ok(): - dct = {'repo': 'a', 'rev': 'b'} - MigrateShaToRev().apply_default(dct) - assert dct == {'repo': 'a', 'rev': 'b'} - - @pytest.mark.parametrize( 'config_repo', ( From 40e69ce8e3fed20290d031c8b660c74da5d4ca3d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 29 Aug 2022 18:58:03 -0400 Subject: [PATCH 705/967] use modules as protocols --- pre_commit/languages/all.py | 84 ++++++++++++++++++++++--------------- testing/gen-languages-all | 30 ------------- tests/repository_test.py | 16 ++++--- 3 files changed, 60 insertions(+), 70 deletions(-) delete mode 100755 testing/gen-languages-all diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index cfd42ce20..7c7c58bde 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -1,7 +1,6 @@ from __future__ import annotations -from typing import Callable -from typing import NamedTuple +from typing import Protocol from typing import Sequence from pre_commit.hook import Hook @@ -27,44 +26,61 @@ from pre_commit.prefix import Prefix -class Language(NamedTuple): - name: str +class Language(Protocol): # Use `None` for no installation / environment - ENVIRONMENT_DIR: str | None + @property + def ENVIRONMENT_DIR(self) -> str | None: ... # return a value to replace `'default` for `language_version` - get_default_version: Callable[[], str] + def get_default_version(self) -> str: ... + # return whether the environment is healthy (or should be rebuilt) - health_check: Callable[[Prefix, str], str | None] + def health_check( + self, + prefix: Prefix, + language_version: str, + ) -> str | None: + ... + # install a repository for the given language and language_version - install_environment: Callable[[Prefix, str, Sequence[str]], None] + def install_environment( + self, + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], + ) -> None: + ... + # execute a hook and return the exit code and output - run_hook: Callable[[Hook, Sequence[str], bool], tuple[int, bytes]] + def run_hook( + self, + hook: Hook, + file_args: Sequence[str], + color: bool, + ) -> tuple[int, bytes]: + ... -# TODO: back to modules + Protocol: https://github.com/python/mypy/issues/5018 -languages = { - # BEGIN GENERATED (testing/gen-languages-all) - 'conda': Language(name='conda', ENVIRONMENT_DIR=conda.ENVIRONMENT_DIR, get_default_version=conda.get_default_version, health_check=conda.health_check, install_environment=conda.install_environment, run_hook=conda.run_hook), # noqa: E501 - 'coursier': Language(name='coursier', ENVIRONMENT_DIR=coursier.ENVIRONMENT_DIR, get_default_version=coursier.get_default_version, health_check=coursier.health_check, install_environment=coursier.install_environment, run_hook=coursier.run_hook), # noqa: E501 - 'dart': Language(name='dart', ENVIRONMENT_DIR=dart.ENVIRONMENT_DIR, get_default_version=dart.get_default_version, health_check=dart.health_check, install_environment=dart.install_environment, run_hook=dart.run_hook), # noqa: E501 - 'docker': Language(name='docker', ENVIRONMENT_DIR=docker.ENVIRONMENT_DIR, get_default_version=docker.get_default_version, health_check=docker.health_check, install_environment=docker.install_environment, run_hook=docker.run_hook), # noqa: E501 - 'docker_image': Language(name='docker_image', ENVIRONMENT_DIR=docker_image.ENVIRONMENT_DIR, get_default_version=docker_image.get_default_version, health_check=docker_image.health_check, install_environment=docker_image.install_environment, run_hook=docker_image.run_hook), # noqa: E501 - 'dotnet': Language(name='dotnet', ENVIRONMENT_DIR=dotnet.ENVIRONMENT_DIR, get_default_version=dotnet.get_default_version, health_check=dotnet.health_check, install_environment=dotnet.install_environment, run_hook=dotnet.run_hook), # noqa: E501 - 'fail': Language(name='fail', ENVIRONMENT_DIR=fail.ENVIRONMENT_DIR, get_default_version=fail.get_default_version, health_check=fail.health_check, install_environment=fail.install_environment, run_hook=fail.run_hook), # noqa: E501 - 'golang': Language(name='golang', ENVIRONMENT_DIR=golang.ENVIRONMENT_DIR, get_default_version=golang.get_default_version, health_check=golang.health_check, install_environment=golang.install_environment, run_hook=golang.run_hook), # noqa: E501 - 'lua': Language(name='lua', ENVIRONMENT_DIR=lua.ENVIRONMENT_DIR, get_default_version=lua.get_default_version, health_check=lua.health_check, install_environment=lua.install_environment, run_hook=lua.run_hook), # noqa: E501 - 'node': Language(name='node', ENVIRONMENT_DIR=node.ENVIRONMENT_DIR, get_default_version=node.get_default_version, health_check=node.health_check, install_environment=node.install_environment, run_hook=node.run_hook), # noqa: E501 - 'perl': Language(name='perl', ENVIRONMENT_DIR=perl.ENVIRONMENT_DIR, get_default_version=perl.get_default_version, health_check=perl.health_check, install_environment=perl.install_environment, run_hook=perl.run_hook), # noqa: E501 - 'pygrep': Language(name='pygrep', ENVIRONMENT_DIR=pygrep.ENVIRONMENT_DIR, get_default_version=pygrep.get_default_version, health_check=pygrep.health_check, install_environment=pygrep.install_environment, run_hook=pygrep.run_hook), # noqa: E501 - 'python': Language(name='python', ENVIRONMENT_DIR=python.ENVIRONMENT_DIR, get_default_version=python.get_default_version, health_check=python.health_check, install_environment=python.install_environment, run_hook=python.run_hook), # noqa: E501 - 'r': Language(name='r', ENVIRONMENT_DIR=r.ENVIRONMENT_DIR, get_default_version=r.get_default_version, health_check=r.health_check, install_environment=r.install_environment, run_hook=r.run_hook), # noqa: E501 - 'ruby': Language(name='ruby', ENVIRONMENT_DIR=ruby.ENVIRONMENT_DIR, get_default_version=ruby.get_default_version, health_check=ruby.health_check, install_environment=ruby.install_environment, run_hook=ruby.run_hook), # noqa: E501 - 'rust': Language(name='rust', ENVIRONMENT_DIR=rust.ENVIRONMENT_DIR, get_default_version=rust.get_default_version, health_check=rust.health_check, install_environment=rust.install_environment, run_hook=rust.run_hook), # noqa: E501 - 'script': Language(name='script', ENVIRONMENT_DIR=script.ENVIRONMENT_DIR, get_default_version=script.get_default_version, health_check=script.health_check, install_environment=script.install_environment, run_hook=script.run_hook), # noqa: E501 - 'swift': Language(name='swift', ENVIRONMENT_DIR=swift.ENVIRONMENT_DIR, get_default_version=swift.get_default_version, health_check=swift.health_check, install_environment=swift.install_environment, run_hook=swift.run_hook), # noqa: E501 - 'system': Language(name='system', ENVIRONMENT_DIR=system.ENVIRONMENT_DIR, get_default_version=system.get_default_version, health_check=system.health_check, install_environment=system.install_environment, run_hook=system.run_hook), # noqa: E501 - # END GENERATED +languages: dict[str, Language] = { + 'conda': conda, + 'coursier': coursier, + 'dart': dart, + 'docker': docker, + 'docker_image': docker_image, + 'dotnet': dotnet, + 'fail': fail, + 'golang': golang, + 'lua': lua, + 'node': node, + 'perl': perl, + 'pygrep': pygrep, + 'python': python, + 'r': r, + 'ruby': ruby, + 'rust': rust, + 'script': script, + 'swift': swift, + 'system': system, + # TODO: fully deprecate `python_venv` + 'python_venv': python, } -# TODO: fully deprecate `python_venv` -languages['python_venv'] = languages['python'] all_languages = sorted(languages) diff --git a/testing/gen-languages-all b/testing/gen-languages-all deleted file mode 100755 index 05f892956..000000000 --- a/testing/gen-languages-all +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import sys - -LANGUAGES = ( - 'conda', 'coursier', 'dart', 'docker', 'docker_image', 'dotnet', 'fail', - 'golang', 'lua', 'node', 'perl', 'pygrep', 'python', 'r', 'ruby', 'rust', - 'script', 'swift', 'system', -) -FIELDS = ( - 'ENVIRONMENT_DIR', 'get_default_version', 'health_check', - 'install_environment', 'run_hook', -) - - -def main() -> int: - print(f' # BEGIN GENERATED ({sys.argv[0]})') - for lang in LANGUAGES: - parts = [f' {lang!r}: Language(name={lang!r}'] - for k in FIELDS: - parts.append(f', {k}={lang}.{k}') - parts.append('), # noqa: E501') - print(''.join(parts)) - print(' # END GENERATED') - return 0 - - -if __name__ == '__main__': - raise SystemExit(main()) diff --git a/tests/repository_test.py b/tests/repository_test.py index c3936bf2f..6aa0f0073 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -133,9 +133,11 @@ def test_python_hook(tempdir_factory, store): def test_python_hook_default_version(tempdir_factory, store): # make sure that this continues to work for platforms where default # language detection does not work - returns_default = mock.Mock(return_value=C.DEFAULT) - lang = languages['python']._replace(get_default_version=returns_default) - with mock.patch.dict(languages, python=lang): + with mock.patch.object( + python, + 'get_default_version', + return_value=C.DEFAULT, + ): test_python_hook(tempdir_factory, store) @@ -247,9 +249,11 @@ def test_run_a_node_hook(tempdir_factory, store): def test_run_a_node_hook_default_version(tempdir_factory, store): # make sure that this continues to work for platforms where node is not # installed at the system - returns_default = mock.Mock(return_value=C.DEFAULT) - lang = languages['node']._replace(get_default_version=returns_default) - with mock.patch.dict(languages, node=lang): + with mock.patch.object( + node, + 'get_default_version', + return_value=C.DEFAULT, + ): test_run_a_node_hook(tempdir_factory, store) From 4a50859936b71a2f38c407a1eb56e74a6978c0a5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 27 Dec 2022 11:47:15 -0500 Subject: [PATCH 706/967] remove pre-commit-validate-config and pre-commit-validate-manifest --- pre_commit/clientlib.py | 39 ------------ pre_commit/commands/validate_config.py | 4 +- pre_commit/commands/validate_manifest.py | 4 +- setup.cfg | 2 - tests/clientlib_test.py | 78 ------------------------ tests/commands/validate_config_test.py | 64 +++++++++++++++++++ tests/commands/validate_manifest_test.py | 18 ++++++ 7 files changed, 88 insertions(+), 121 deletions(-) create mode 100644 tests/commands/validate_config_test.py create mode 100644 tests/commands/validate_manifest_test.py diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index df8d2e2d0..deb160a0e 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -1,6 +1,5 @@ from __future__ import annotations -import argparse import functools import logging import re @@ -13,12 +12,8 @@ from identify.identify import ALL_TAGS import pre_commit.constants as C -from pre_commit.color import add_color_option -from pre_commit.commands.validate_config import validate_config -from pre_commit.commands.validate_manifest import validate_manifest from pre_commit.errors import FatalError from pre_commit.languages.all import all_languages -from pre_commit.logging_handler import logging_handler from pre_commit.util import parse_version from pre_commit.util import yaml_load @@ -44,14 +39,6 @@ def check_min_version(version: str) -> None: ) -def _make_argparser(filenames_help: str) -> argparse.ArgumentParser: - parser = argparse.ArgumentParser() - parser.add_argument('filenames', nargs='*', help=filenames_help) - parser.add_argument('-V', '--version', action='version', version=C.VERSION) - add_color_option(parser) - return parser - - MANIFEST_HOOK_DICT = cfgv.Map( 'Hook', 'id', @@ -97,19 +84,6 @@ class InvalidManifestError(FatalError): ) -def validate_manifest_main(argv: Sequence[str] | None = None) -> int: - parser = _make_argparser('Manifest filenames.') - args = parser.parse_args(argv) - - with logging_handler(args.color): - logger.warning( - 'pre-commit-validate-manifest is deprecated -- ' - 'use `pre-commit validate-manifest` instead.', - ) - - return validate_manifest(args.filenames) - - LOCAL = 'local' META = 'meta' @@ -397,16 +371,3 @@ class InvalidConfigError(FatalError): load_strategy=yaml_load, exc_tp=InvalidConfigError, ) - - -def validate_config_main(argv: Sequence[str] | None = None) -> int: - parser = _make_argparser('Config filenames.') - args = parser.parse_args(argv) - - with logging_handler(args.color): - logger.warning( - 'pre-commit-validate-config is deprecated -- ' - 'use `pre-commit validate-config` instead.', - ) - - return validate_config(args.filenames) diff --git a/pre_commit/commands/validate_config.py b/pre_commit/commands/validate_config.py index 91bb017a3..24bd3135e 100644 --- a/pre_commit/commands/validate_config.py +++ b/pre_commit/commands/validate_config.py @@ -1,9 +1,11 @@ from __future__ import annotations +from typing import Sequence + from pre_commit import clientlib -def validate_config(filenames: list[str]) -> int: +def validate_config(filenames: Sequence[str]) -> int: ret = 0 for filename in filenames: diff --git a/pre_commit/commands/validate_manifest.py b/pre_commit/commands/validate_manifest.py index 372a6380f..419031a9b 100644 --- a/pre_commit/commands/validate_manifest.py +++ b/pre_commit/commands/validate_manifest.py @@ -1,9 +1,11 @@ from __future__ import annotations +from typing import Sequence + from pre_commit import clientlib -def validate_manifest(filenames: list[str]) -> int: +def validate_manifest(filenames: Sequence[str]) -> int: ret = 0 for filename in filenames: diff --git a/setup.cfg b/setup.cfg index 1d28a41c8..ca1f7d8bd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,8 +34,6 @@ exclude = [options.entry_points] console_scripts = pre-commit = pre_commit.main:main - pre-commit-validate-config = pre_commit.clientlib:validate_config_main - pre-commit-validate-manifest = pre_commit.clientlib:validate_manifest_main [options.package_data] pre_commit.resources = diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 23d9352f0..2afeaeced 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -17,8 +17,6 @@ from pre_commit.clientlib import MigrateShaToRev from pre_commit.clientlib import OptionalSensibleRegexAtHook from pre_commit.clientlib import OptionalSensibleRegexAtTop -from pre_commit.clientlib import validate_config_main -from pre_commit.clientlib import validate_manifest_main from testing.fixtures import sample_local_config @@ -112,70 +110,6 @@ def test_config_schema_does_not_contain_defaults(): assert not isinstance(item, cfgv.Optional) -def test_validate_manifest_main_ok(): - assert not validate_manifest_main(('.pre-commit-hooks.yaml',)) - - -def test_validate_config_main_ok(): - assert not validate_config_main(('.pre-commit-config.yaml',)) - - -def test_validate_warn_on_unknown_keys_at_repo_level(tmpdir, caplog): - f = tmpdir.join('cfg.yaml') - f.write( - 'repos:\n' - '- repo: https://gitlab.com/pycqa/flake8\n' - ' rev: 3.7.7\n' - ' hooks:\n' - ' - id: flake8\n' - ' args: [--some-args]\n', - ) - ret_val = validate_config_main((f.strpath,)) - assert not ret_val - assert caplog.record_tuples == [ - ( - 'pre_commit', - logging.WARNING, - 'pre-commit-validate-config is deprecated -- ' - 'use `pre-commit validate-config` instead.', - ), - ( - 'pre_commit', - logging.WARNING, - 'Unexpected key(s) present on https://gitlab.com/pycqa/flake8: ' - 'args', - ), - ] - - -def test_validate_warn_on_unknown_keys_at_top_level(tmpdir, caplog): - f = tmpdir.join('cfg.yaml') - f.write( - 'repos:\n' - '- repo: https://gitlab.com/pycqa/flake8\n' - ' rev: 3.7.7\n' - ' hooks:\n' - ' - id: flake8\n' - 'foo:\n' - ' id: 1.0.0\n', - ) - ret_val = validate_config_main((f.strpath,)) - assert not ret_val - assert caplog.record_tuples == [ - ( - 'pre_commit', - logging.WARNING, - 'pre-commit-validate-config is deprecated -- ' - 'use `pre-commit validate-config` instead.', - ), - ( - 'pre_commit', - logging.WARNING, - 'Unexpected key(s) present at root: foo', - ), - ] - - def test_ci_map_key_allowed_at_top_level(caplog): cfg = { 'ci': {'skip': ['foo']}, @@ -362,18 +296,6 @@ def test_validate_optional_sensible_regex_at_top_level(caplog, regex, warning): assert caplog.record_tuples == [('pre_commit', logging.WARNING, warning)] -@pytest.mark.parametrize('fn', (validate_config_main, validate_manifest_main)) -def test_mains_not_ok(tmpdir, fn): - not_yaml = tmpdir.join('f.notyaml') - not_yaml.write('{') - not_schema = tmpdir.join('notconfig.yaml') - not_schema.write('{}') - - assert fn(('does-not-exist',)) - assert fn((not_yaml.strpath,)) - assert fn((not_schema.strpath,)) - - @pytest.mark.parametrize( ('manifest_obj', 'expected'), ( diff --git a/tests/commands/validate_config_test.py b/tests/commands/validate_config_test.py new file mode 100644 index 000000000..a475cd814 --- /dev/null +++ b/tests/commands/validate_config_test.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import logging + +from pre_commit.commands.validate_config import validate_config + + +def test_validate_config_ok(): + assert not validate_config(('.pre-commit-config.yaml',)) + + +def test_validate_warn_on_unknown_keys_at_repo_level(tmpdir, caplog): + f = tmpdir.join('cfg.yaml') + f.write( + 'repos:\n' + '- repo: https://gitlab.com/pycqa/flake8\n' + ' rev: 3.7.7\n' + ' hooks:\n' + ' - id: flake8\n' + ' args: [--some-args]\n', + ) + ret_val = validate_config((f.strpath,)) + assert not ret_val + assert caplog.record_tuples == [ + ( + 'pre_commit', + logging.WARNING, + 'Unexpected key(s) present on https://gitlab.com/pycqa/flake8: ' + 'args', + ), + ] + + +def test_validate_warn_on_unknown_keys_at_top_level(tmpdir, caplog): + f = tmpdir.join('cfg.yaml') + f.write( + 'repos:\n' + '- repo: https://gitlab.com/pycqa/flake8\n' + ' rev: 3.7.7\n' + ' hooks:\n' + ' - id: flake8\n' + 'foo:\n' + ' id: 1.0.0\n', + ) + ret_val = validate_config((f.strpath,)) + assert not ret_val + assert caplog.record_tuples == [ + ( + 'pre_commit', + logging.WARNING, + 'Unexpected key(s) present at root: foo', + ), + ] + + +def test_mains_not_ok(tmpdir): + not_yaml = tmpdir.join('f.notyaml') + not_yaml.write('{') + not_schema = tmpdir.join('notconfig.yaml') + not_schema.write('{}') + + assert validate_config(('does-not-exist',)) + assert validate_config((not_yaml.strpath,)) + assert validate_config((not_schema.strpath,)) diff --git a/tests/commands/validate_manifest_test.py b/tests/commands/validate_manifest_test.py new file mode 100644 index 000000000..a4bc8ac05 --- /dev/null +++ b/tests/commands/validate_manifest_test.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from pre_commit.commands.validate_manifest import validate_manifest + + +def test_validate_manifest_ok(): + assert not validate_manifest(('.pre-commit-hooks.yaml',)) + + +def test_not_ok(tmpdir): + not_yaml = tmpdir.join('f.notyaml') + not_yaml.write('{') + not_schema = tmpdir.join('notconfig.yaml') + not_schema.write('{}') + + assert validate_manifest(('does-not-exist',)) + assert validate_manifest((not_yaml.strpath,)) + assert validate_manifest((not_schema.strpath,)) From 887c5e1142ea9022407e85e7319bec3a403e1572 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 29 Dec 2022 20:54:03 -0500 Subject: [PATCH 707/967] azure pipelines -> github actions --- .github/actions/pre-test/action.yml | 41 +++++++++++++++++ .github/workflows/main.yml | 23 ++++++++++ README.md | 3 +- azure-pipelines.yml | 68 ----------------------------- testing/get-coursier.sh | 2 +- testing/get-dart.sh | 4 +- testing/get-lua.sh | 5 --- testing/get-swift.sh | 2 +- 8 files changed, 69 insertions(+), 79 deletions(-) create mode 100644 .github/actions/pre-test/action.yml create mode 100644 .github/workflows/main.yml delete mode 100644 azure-pipelines.yml delete mode 100755 testing/get-lua.sh diff --git a/.github/actions/pre-test/action.yml b/.github/actions/pre-test/action.yml new file mode 100644 index 000000000..f230df7b0 --- /dev/null +++ b/.github/actions/pre-test/action.yml @@ -0,0 +1,41 @@ +inputs: + env: + default: ${{ matrix.env }} + +runs: + using: composite + steps: + - name: setup (windows) + shell: bash + if: runner.os == 'Windows' + run: | + set -x + + echo 'TEMP=C:\TEMP' >> "$GITHUB_ENV" + + echo "$CONDA\Scripts" >> "$GITHUB_PATH" + + echo 'C:\Strawberry\perl\bin' >> "$GITHUB_PATH" + echo 'C:\Strawberry\perl\site\bin' >> "$GITHUB_PATH" + echo 'C:\Strawberry\c\bin' >> "$GITHUB_PATH" + + testing/get-dart.sh + pwsh testing/get-r.ps1 + - name: setup (linux) + shell: bash + if: runner.os == 'Linux' + run: | + set -x + + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + lua5.3 \ + liblua5.3-dev \ + luarocks \ + r-base + + testing/get-coursier.sh + testing/get-dart.sh + testing/get-swift.sh + - uses: asottile/workflows/.github/actions/latest-git@v1.2.0 + if: inputs.env == 'py38' && runner.os == 'Linux' diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 000000000..c78d1051c --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,23 @@ +name: main + +on: + push: + branches: [main, test-me-*] + tags: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + main-windows: + uses: asottile/workflows/.github/workflows/tox.yml@v1.2.0 + with: + env: '["py38"]' + os: windows-latest + main-linux: + uses: asottile/workflows/.github/workflows/tox.yml@v1.2.0 + with: + env: '["py38", "py39", "py310"]' + os: ubuntu-latest diff --git a/README.md b/README.md index db1259c25..0c81a7890 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -[![Build Status](https://dev.azure.com/asottile/asottile/_apis/build/status/pre-commit.pre-commit?branchName=main)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=main) -[![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/21/main.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=main) +[![build status](https://github.com/pre-commit/pre-commit/actions/workflows/main.yml/badge.svg)](https://github.com/pre-commit/pre-commit/actions/workflows/main.yml) [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/pre-commit/pre-commit/main.svg)](https://results.pre-commit.ci/latest/github/pre-commit/pre-commit/main) ## pre-commit diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index 911ef32d5..000000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,68 +0,0 @@ -trigger: - branches: - include: [main, test-me-*] - tags: - include: ['*'] - -resources: - repositories: - - repository: asottile - type: github - endpoint: github - name: asottile/azure-pipeline-templates - ref: refs/tags/v2.4.1 - -jobs: -- template: job--python-tox.yml@asottile - parameters: - toxenvs: [py38] - os: windows - additional_variables: - TEMP: C:\Temp - pre_test: - - task: UseRubyVersion@0 - - powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts" - displayName: Add conda to PATH - - powershell: | - Write-Host "##vso[task.prependpath]C:\Strawberry\perl\bin" - Write-Host "##vso[task.prependpath]C:\Strawberry\perl\site\bin" - Write-Host "##vso[task.prependpath]C:\Strawberry\c\bin" - displayName: Add strawberry perl to PATH - - bash: testing/get-dart.sh - displayName: install dart - - powershell: testing/get-r.ps1 - displayName: install R -- template: job--python-tox.yml@asottile - parameters: - toxenvs: [py38] - os: linux - name_postfix: _latest_git - pre_test: - - task: UseRubyVersion@0 - - template: step--git-install.yml - - bash: testing/get-coursier.sh - displayName: install coursier - - bash: testing/get-dart.sh - displayName: install dart - - bash: testing/get-lua.sh - displayName: install lua - - bash: testing/get-swift.sh - displayName: install swift - - bash: testing/get-r.sh - displayName: install R -- template: job--python-tox.yml@asottile - parameters: - toxenvs: [py38, py39, py310] - os: linux - pre_test: - - task: UseRubyVersion@0 - - bash: testing/get-coursier.sh - displayName: install coursier - - bash: testing/get-dart.sh - displayName: install dart - - bash: testing/get-lua.sh - displayName: install lua - - bash: testing/get-swift.sh - displayName: install swift - - bash: testing/get-r.sh - displayName: install R diff --git a/testing/get-coursier.sh b/testing/get-coursier.sh index 4c5e955de..6033c3e35 100755 --- a/testing/get-coursier.sh +++ b/testing/get-coursier.sh @@ -12,4 +12,4 @@ curl --location --silent --output "$ARTIFACT" "$COURSIER_URL" echo "$COURSIER_HASH $ARTIFACT" | sha256sum --check chmod ugo+x /tmp/coursier/cs -echo '##vso[task.prependpath]/tmp/coursier' +echo '/tmp/coursier' >> "$GITHUB_PATH" diff --git a/testing/get-dart.sh b/testing/get-dart.sh index b655e1a8d..998b9d98f 100755 --- a/testing/get-dart.sh +++ b/testing/get-dart.sh @@ -5,10 +5,10 @@ VERSION=2.13.4 if [ "$OSTYPE" = msys ]; then URL="https://storage.googleapis.com/dart-archive/channels/stable/release/${VERSION}/sdk/dartsdk-windows-x64-release.zip" - echo "##vso[task.prependpath]$(cygpath -w /tmp/dart-sdk/bin)" + cygpath -w /tmp/dart-sdk/bin >> "$GITHUB_PATH" else URL="https://storage.googleapis.com/dart-archive/channels/stable/release/${VERSION}/sdk/dartsdk-linux-x64-release.zip" - echo '##vso[task.prependpath]/tmp/dart-sdk/bin' + echo '/tmp/dart-sdk/bin' >> "$GITHUB_PATH" fi curl --silent --location --output /tmp/dart.zip "$URL" diff --git a/testing/get-lua.sh b/testing/get-lua.sh deleted file mode 100755 index 580e24772..000000000 --- a/testing/get-lua.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Install the runtime and package manager. -sudo apt install lua5.3 liblua5.3-dev luarocks diff --git a/testing/get-swift.sh b/testing/get-swift.sh index 3e7808241..dfe093912 100755 --- a/testing/get-swift.sh +++ b/testing/get-swift.sh @@ -26,4 +26,4 @@ fi mkdir -p /tmp/swift tar -xf "$TGZ" --strip 1 --directory /tmp/swift -echo '##vso[task.prependpath]/tmp/swift/usr/bin' +echo '/tmp/swift/usr/bin' >> "$GITHUB_PATH" From cddaa0dddc7e1fab506795287007aff50e88b592 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 29 Dec 2022 22:56:58 -0500 Subject: [PATCH 708/967] r is installed by default on GHA --- .github/actions/pre-test/action.yml | 4 +--- testing/get-r.ps1 | 6 ------ testing/get-r.sh | 9 --------- 3 files changed, 1 insertion(+), 18 deletions(-) delete mode 100644 testing/get-r.ps1 delete mode 100755 testing/get-r.sh diff --git a/.github/actions/pre-test/action.yml b/.github/actions/pre-test/action.yml index f230df7b0..a7bf0abed 100644 --- a/.github/actions/pre-test/action.yml +++ b/.github/actions/pre-test/action.yml @@ -20,7 +20,6 @@ runs: echo 'C:\Strawberry\c\bin' >> "$GITHUB_PATH" testing/get-dart.sh - pwsh testing/get-r.ps1 - name: setup (linux) shell: bash if: runner.os == 'Linux' @@ -31,8 +30,7 @@ runs: sudo apt-get install -y --no-install-recommends \ lua5.3 \ liblua5.3-dev \ - luarocks \ - r-base + luarocks testing/get-coursier.sh testing/get-dart.sh diff --git a/testing/get-r.ps1 b/testing/get-r.ps1 deleted file mode 100644 index e7b7b6195..000000000 --- a/testing/get-r.ps1 +++ /dev/null @@ -1,6 +0,0 @@ -$dir = $Env:Temp -$urlR = "https://cran.r-project.org/bin/windows/base/old/4.0.4/R-4.0.4-win.exe" -$outputR = "$dir\R-win.exe" -$wcR = New-Object System.Net.WebClient -$wcR.DownloadFile($urlR, $outputR) -Start-Process -FilePath $outputR -ArgumentList "/S /v/qn" diff --git a/testing/get-r.sh b/testing/get-r.sh deleted file mode 100755 index 5d09828e4..000000000 --- a/testing/get-r.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash -sudo apt install r-base -# create empty folder for user library. -# necessary for non-root users who have -# never installed an R package before. -# Alternatively, we require the renv -# package to be installed already, then we can -# omit that. -Rscript -e 'dir.create(Sys.getenv("R_LIBS_USER"), recursive = TRUE)' From 0a0754e44a3b6bc3d2e56353f5143d5905d45f97 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 1 Jan 2023 17:12:28 -0500 Subject: [PATCH 709/967] special rmtree is not needed for TemporaryDirectory in 3.8+ --- pre_commit/commands/autoupdate.py | 4 ++-- pre_commit/commands/try_repo.py | 4 ++-- pre_commit/util.py | 13 ------------- tests/util_test.py | 7 ------- 4 files changed, 4 insertions(+), 24 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index d5352e5e7..6da53112e 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -2,6 +2,7 @@ import os.path import re +import tempfile from typing import Any from typing import NamedTuple from typing import Sequence @@ -19,7 +20,6 @@ from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b -from pre_commit.util import tmpdir from pre_commit.util import yaml_dump from pre_commit.util import yaml_load @@ -47,7 +47,7 @@ def update(self, tags_only: bool, freeze: bool) -> RevInfo: 'FETCH_HEAD', '--tags', '--exact', ) - with tmpdir() as tmp: + with tempfile.TemporaryDirectory() as tmp: git.init_repo(tmp, self.repo) cmd_output_b( *git_cmd, 'fetch', 'origin', 'HEAD', '--tags', diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index ef099f5e3..5244aeff4 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -3,6 +3,7 @@ import argparse import logging import os.path +import tempfile import pre_commit.constants as C from pre_commit import git @@ -11,7 +12,6 @@ from pre_commit.commands.run import run from pre_commit.store import Store from pre_commit.util import cmd_output_b -from pre_commit.util import tmpdir from pre_commit.util import yaml_dump from pre_commit.xargs import xargs @@ -49,7 +49,7 @@ def _repo_ref(tmpdir: str, repo: str, ref: str | None) -> tuple[str, str]: def try_repo(args: argparse.Namespace) -> int: - with tmpdir() as tempdir: + with tempfile.TemporaryDirectory() as tempdir: repo, ref = _repo_ref(tempdir, args.repo, args.ref) store = Store(tempdir) diff --git a/pre_commit/util.py b/pre_commit/util.py index b85076883..bca89bb74 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -9,7 +9,6 @@ import stat import subprocess import sys -import tempfile from types import TracebackType from typing import Any from typing import Callable @@ -52,18 +51,6 @@ def clean_path_on_failure(path: str) -> Generator[None, None, None]: raise -@contextlib.contextmanager -def tmpdir() -> Generator[str, None, None]: - """Contextmanager to create a temporary directory. It will be cleaned up - afterwards. - """ - tempdir = tempfile.mkdtemp() - try: - yield tempdir - finally: - rmtree(tempdir) - - def resource_bytesio(filename: str) -> IO[bytes]: return importlib.resources.open_binary('pre_commit.resources', filename) diff --git a/tests/util_test.py b/tests/util_test.py index b3f18b4cf..415982d01 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -14,7 +14,6 @@ from pre_commit.util import make_executable from pre_commit.util import parse_version from pre_commit.util import rmtree -from pre_commit.util import tmpdir def test_CalledProcessError_str(): @@ -74,12 +73,6 @@ class MySystemExit(SystemExit): assert not os.path.exists('foo') -def test_tmpdir(): - with tmpdir() as tempdir: - assert os.path.exists(tempdir) - assert not os.path.exists(tempdir) - - def test_cmd_output_exe_not_found(): ret, out, _ = cmd_output('dne', check=False) assert ret == 1 From 5425c754a0cdfe9f35df6d5de49c41bc9fb3413c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 1 Jan 2023 17:17:00 -0500 Subject: [PATCH 710/967] move parse_version to pre_commit.clientlib --- pre_commit/clientlib.py | 6 +++++- pre_commit/repository.py | 2 +- pre_commit/util.py | 5 ----- tests/clientlib_test.py | 7 +++++++ tests/util_test.py | 7 ------- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 5b0bdbbdf..e03d5d666 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -14,7 +14,6 @@ import pre_commit.constants as C from pre_commit.errors import FatalError from pre_commit.languages.all import all_languages -from pre_commit.util import parse_version from pre_commit.util import yaml_load logger = logging.getLogger('pre_commit') @@ -30,6 +29,11 @@ def check_type_tag(tag: str) -> None: ) +def parse_version(s: str) -> tuple[int, ...]: + """poor man's version comparison""" + return tuple(int(p) for p in s.split('.')) + + def check_min_version(version: str) -> None: if parse_version(version) > parse_version(C.VERSION): raise cfgv.ValidationError( diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 4092277a8..7670f9970 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -10,12 +10,12 @@ from pre_commit.clientlib import load_manifest from pre_commit.clientlib import LOCAL from pre_commit.clientlib import META +from pre_commit.clientlib import parse_version from pre_commit.hook import Hook from pre_commit.languages.all import languages from pre_commit.languages.helpers import environment_dir from pre_commit.prefix import Prefix from pre_commit.store import Store -from pre_commit.util import parse_version from pre_commit.util import rmtree diff --git a/pre_commit/util.py b/pre_commit/util.py index b85076883..0aa6cccba 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -254,10 +254,5 @@ def handle_remove_readonly( shutil.rmtree(path, ignore_errors=False, onerror=handle_remove_readonly) -def parse_version(s: str) -> tuple[int, ...]: - """poor man's version comparison""" - return tuple(int(p) for p in s.split('.')) - - def win_exe(s: str) -> str: return s if sys.platform != 'win32' else f'{s}.exe' diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 12694e4d8..efb2aa84a 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -16,6 +16,7 @@ from pre_commit.clientlib import META_HOOK_DICT from pre_commit.clientlib import OptionalSensibleRegexAtHook from pre_commit.clientlib import OptionalSensibleRegexAtTop +from pre_commit.clientlib import parse_version from testing.fixtures import sample_local_config @@ -384,6 +385,12 @@ def test_default_language_version_invalid(mapping): cfgv.validate(mapping, DEFAULT_LANGUAGE_VERSION) +def test_parse_version(): + assert parse_version('0.0') == parse_version('0.0') + assert parse_version('0.1') > parse_version('0.0') + assert parse_version('2.1') >= parse_version('2') + + def test_minimum_pre_commit_version_failing(): with pytest.raises(cfgv.ValidationError) as excinfo: cfg = {'repos': [], 'minimum_pre_commit_version': '999'} diff --git a/tests/util_test.py b/tests/util_test.py index b3f18b4cf..26dafc34d 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -12,7 +12,6 @@ from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_p from pre_commit.util import make_executable -from pre_commit.util import parse_version from pre_commit.util import rmtree from pre_commit.util import tmpdir @@ -105,12 +104,6 @@ def test_cmd_output_no_shebang(tmpdir, fn): assert out.endswith(b'\n') -def test_parse_version(): - assert parse_version('0.0') == parse_version('0.0') - assert parse_version('0.1') > parse_version('0.0') - assert parse_version('2.1') >= parse_version('2') - - def test_rmtree_read_only_directories(tmpdir): """Simulates the go module tree. See #1042""" tmpdir.join('x/y/z').ensure_dir().join('a').ensure() From 8e57e8075dc4adcacf9b8dd49abc8c0b6e50f9e0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 1 Jan 2023 18:14:55 -0500 Subject: [PATCH 711/967] avoid using hook.src in R language this wasn't meant to be read -- hook.prefix works fine for local too --- pre_commit/languages/r.py | 18 ++++----------- tests/languages/r_test.py | 48 ++++++++++++++------------------------- 2 files changed, 22 insertions(+), 44 deletions(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index d281102b2..c050d451b 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -44,19 +44,11 @@ def _get_env_dir(prefix: Prefix, version: str) -> str: return prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) -def _prefix_if_non_local_file_entry( - entry: Sequence[str], - prefix: Prefix, - src: str, -) -> Sequence[str]: +def _prefix_if_file_entry(entry: list[str], prefix: Prefix) -> Sequence[str]: if entry[1] == '-e': return entry[1:] else: - if src == 'local': - path = entry[1] - else: - path = prefix.path(entry[1]) - return (path,) + return (prefix.path(entry[1]),) def _rscript_exec() -> str: @@ -67,7 +59,7 @@ def _rscript_exec() -> str: return os.path.join(r_home, 'bin', win_exe('Rscript')) -def _entry_validate(entry: Sequence[str]) -> None: +def _entry_validate(entry: list[str]) -> None: """ Allowed entries: # Rscript -e expr @@ -91,8 +83,8 @@ def _cmd_from_hook(hook: Hook) -> tuple[str, ...]: _entry_validate(entry) return ( - *entry[:1], *RSCRIPT_OPTS, - *_prefix_if_non_local_file_entry(entry, hook.prefix, hook.src), + entry[0], *RSCRIPT_OPTS, + *_prefix_if_file_entry(entry, hook.prefix), *hook.args, ) diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py index c52d5acd3..c653a3ccf 100644 --- a/tests/languages/r_test.py +++ b/tests/languages/r_test.py @@ -16,27 +16,18 @@ def _test_r_parsing( tempdir_factory, store, hook_id, - expected_hook_expr={}, - expected_args={}, - config={}, - expect_path_prefix=True, + expected_hook_expr=(), + expected_args=(), + config=None, ): - repo_path = 'r_hooks_repo' - path = make_repo(tempdir_factory, repo_path) - config = config or make_config_from_repo(path) + repo = make_repo(tempdir_factory, 'r_hooks_repo') + config = make_config_from_repo(repo) hook = _get_hook_no_install(config, store, hook_id) ret = r._cmd_from_hook(hook) - expected_cmd = 'Rscript' - expected_opts = ( - '--no-save', '--no-restore', '--no-site-file', '--no-environ', - ) - expected_path = os.path.join( - hook.prefix.prefix_dir if expect_path_prefix else '', - f'{hook_id}.R', - ) + expected_path = os.path.join(hook.prefix.prefix_dir, f'{hook_id}.R') expected = ( - expected_cmd, - *expected_opts, + 'Rscript', + '--no-save', '--no-restore', '--no-site-file', '--no-environ', *(expected_hook_expr or (expected_path,)), *expected_args, ) @@ -84,9 +75,7 @@ def test_r_parsing_expr_no_opts_no_args2(tempdir_factory, store): def test_r_parsing_expr_opts_no_args2(tempdir_factory, store): with pytest.raises(ValueError) as execinfo: r._entry_validate( - [ - 'Rscript', '--vanilla', '-e', '1+1', '-e', 'letters', - ], + ['Rscript', '--vanilla', '-e', '1+1', '-e', 'letters'], ) msg = execinfo.value.args assert msg == ( @@ -112,24 +101,21 @@ def test_r_parsing_expr_non_Rscirpt(tempdir_factory, store): def test_r_parsing_file_local(tempdir_factory, store): - path = 'path/to/script.R' - hook_id = 'local-r' config = { 'repo': 'local', 'hooks': [{ - 'id': hook_id, + 'id': 'local-r', 'name': 'local-r', - 'entry': f'Rscript {path}', + 'entry': 'Rscript path/to/script.R', 'language': 'r', }], } - _test_r_parsing( - tempdir_factory, - store, - hook_id=hook_id, - expected_hook_expr=(path,), - config=config, - expect_path_prefix=False, + hook = _get_hook_no_install(config, store, 'local-r') + ret = r._cmd_from_hook(hook) + assert ret == ( + 'Rscript', + '--no-save', '--no-restore', '--no-site-file', '--no-environ', + hook.prefix.path('path/to/script.R'), ) From f0baffb01fe8efe200f103fccd4a5842860095cd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 1 Jan 2023 19:20:40 -0500 Subject: [PATCH 712/967] remove None overload for environment_dir --- pre_commit/languages/helpers.py | 14 ++------------ pre_commit/repository.py | 15 ++++++++------- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 0be08b54b..d462e86ca 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -6,7 +6,6 @@ import re from typing import Any from typing import NoReturn -from typing import overload from typing import Sequence import pre_commit.constants as C @@ -48,17 +47,8 @@ def run_setup_cmd(prefix: Prefix, cmd: tuple[str, ...], **kwargs: Any) -> None: cmd_output_b(*cmd, cwd=prefix.prefix_dir, **kwargs) -@overload -def environment_dir(d: None, language_version: str) -> None: ... -@overload -def environment_dir(d: str, language_version: str) -> str: ... - - -def environment_dir(d: str | None, language_version: str) -> str | None: - if d is None: - return None - else: - return f'{d}-{language_version}' +def environment_dir(d: str, language_version: str) -> str: + return f'{d}-{language_version}' def assert_version_default(binary: str, version: str) -> None: diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 7670f9970..50bc64552 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -50,15 +50,16 @@ def _write_state(prefix: Prefix, venv: str, state: object) -> None: def _hook_installed(hook: Hook) -> bool: lang = languages[hook.language] + if lang.ENVIRONMENT_DIR is None: + return True + venv = environment_dir(lang.ENVIRONMENT_DIR, hook.language_version) return ( - venv is None or ( - ( - _read_state(hook.prefix, venv) == - _state(hook.additional_dependencies) - ) and - not lang.health_check(hook.prefix, hook.language_version) - ) + ( + _read_state(hook.prefix, venv) == + _state(hook.additional_dependencies) + ) and + not lang.health_check(hook.prefix, hook.language_version) ) From d05b7888ab7fa4cc74f55e050f0f57442df2250d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 1 Jan 2023 19:46:17 -0500 Subject: [PATCH 713/967] move clean_path_on_failure out of each hook install --- pre_commit/languages/conda.py | 16 ++--- pre_commit/languages/coursier.py | 30 ++++----- pre_commit/languages/dart.py | 56 ++++++++-------- pre_commit/languages/docker.py | 6 +- pre_commit/languages/dotnet.py | 106 +++++++++++++++---------------- pre_commit/languages/golang.py | 50 +++++++-------- pre_commit/languages/lua.py | 32 +++++----- pre_commit/languages/node.py | 55 ++++++++-------- pre_commit/languages/perl.py | 10 ++- pre_commit/languages/python.py | 8 +-- pre_commit/languages/r.py | 94 ++++++++++++++------------- pre_commit/languages/ruby.py | 50 +++++++-------- pre_commit/languages/rust.py | 38 ++++++----- pre_commit/languages/swift.py | 16 ++--- pre_commit/repository.py | 60 +++++++++-------- 15 files changed, 296 insertions(+), 331 deletions(-) diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index f0195e4f7..76ae0781f 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -13,7 +13,6 @@ from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix -from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'conda' @@ -71,16 +70,15 @@ def install_environment( conda_exe = _conda_exe() env_dir = prefix.path(directory) - with clean_path_on_failure(env_dir): + cmd_output_b( + conda_exe, 'env', 'create', '-p', env_dir, '--file', + 'environment.yml', cwd=prefix.prefix_dir, + ) + if additional_dependencies: cmd_output_b( - conda_exe, 'env', 'create', '-p', env_dir, '--file', - 'environment.yml', cwd=prefix.prefix_dir, + conda_exe, 'install', '-p', env_dir, *additional_dependencies, + cwd=prefix.prefix_dir, ) - if additional_dependencies: - cmd_output_b( - conda_exe, 'install', '-p', env_dir, *additional_dependencies, - cwd=prefix.prefix_dir, - ) def run_hook( diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index 9fe43ebd8..0d520f0fb 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -12,7 +12,6 @@ from pre_commit.languages import helpers from pre_commit.parse_shebang import find_executable from pre_commit.prefix import Prefix -from pre_commit.util import clean_path_on_failure ENVIRONMENT_DIR = 'coursier' @@ -38,21 +37,20 @@ def install_environment( envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) channel = prefix.path('.pre-commit-channel') - with clean_path_on_failure(envdir): - for app_descriptor in os.listdir(channel): - _, app_file = os.path.split(app_descriptor) - app, _ = os.path.splitext(app_file) - helpers.run_setup_cmd( - prefix, - ( - executable, - 'install', - '--default-channels=false', - f'--channel={channel}', - app, - f'--dir={envdir}', - ), - ) + for app_descriptor in os.listdir(channel): + _, app_file = os.path.split(app_descriptor) + app, _ = os.path.splitext(app_file) + helpers.run_setup_cmd( + prefix, + ( + executable, + 'install', + '--default-channels=false', + f'--channel={channel}', + app, + f'--dir={envdir}', + ), + ) def get_env_patch(target_dir: str) -> PatchesT: # pragma: win32 no cover diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py index 55ecbf4fd..73fffdb86 100644 --- a/pre_commit/languages/dart.py +++ b/pre_commit/languages/dart.py @@ -14,7 +14,6 @@ from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix -from pre_commit.util import clean_path_on_failure from pre_commit.util import win_exe from pre_commit.util import yaml_load @@ -67,38 +66,37 @@ def _install_dir(prefix_p: Prefix, pub_cache: str) -> None: env=dart_env, ) - with clean_path_on_failure(envdir): - os.makedirs(bin_dir) + os.makedirs(bin_dir) - with tempfile.TemporaryDirectory() as tmp: - _install_dir(prefix, tmp) + with tempfile.TemporaryDirectory() as tmp: + _install_dir(prefix, tmp) - for dep_s in additional_dependencies: - with tempfile.TemporaryDirectory() as dep_tmp: - dep, _, version = dep_s.partition(':') - if version: - dep_cmd: tuple[str, ...] = (dep, '--version', version) - else: - dep_cmd = (dep,) + for dep_s in additional_dependencies: + with tempfile.TemporaryDirectory() as dep_tmp: + dep, _, version = dep_s.partition(':') + if version: + dep_cmd: tuple[str, ...] = (dep, '--version', version) + else: + dep_cmd = (dep,) - helpers.run_setup_cmd( - prefix, - ('dart', 'pub', 'cache', 'add', *dep_cmd), - env={**os.environ, 'PUB_CACHE': dep_tmp}, - ) + helpers.run_setup_cmd( + prefix, + ('dart', 'pub', 'cache', 'add', *dep_cmd), + env={**os.environ, 'PUB_CACHE': dep_tmp}, + ) - # try and find the 'pubspec.yaml' that just got added - for root, _, filenames in os.walk(dep_tmp): - if 'pubspec.yaml' in filenames: - with tempfile.TemporaryDirectory() as copied: - pkg = os.path.join(copied, 'pkg') - shutil.copytree(root, pkg) - _install_dir(Prefix(pkg), dep_tmp) - break - else: - raise AssertionError( - f'could not find pubspec.yaml for {dep_s}', - ) + # try and find the 'pubspec.yaml' that just got added + for root, _, filenames in os.walk(dep_tmp): + if 'pubspec.yaml' in filenames: + with tempfile.TemporaryDirectory() as copied: + pkg = os.path.join(copied, 'pkg') + shutil.copytree(root, pkg) + _install_dir(Prefix(pkg), dep_tmp) + break + else: + raise AssertionError( + f'could not find pubspec.yaml for {dep_s}', + ) def run_hook( diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index eea9f7682..5d6146740 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -10,7 +10,6 @@ from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError -from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'docker' @@ -101,9 +100,8 @@ def install_environment( # Docker doesn't really have relevant disk environment, but pre-commit # still needs to cleanup its state files on failure - with clean_path_on_failure(directory): - build_docker_image(prefix, pull=True) - os.mkdir(directory) + build_docker_image(prefix, pull=True) + os.mkdir(directory) def get_docker_user() -> tuple[str, ...]: # pragma: win32 no cover diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index e26b45c3a..d748c8131 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -16,7 +16,6 @@ from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix -from pre_commit.util import clean_path_on_failure ENVIRONMENT_DIR = 'dotnetenv' BIN_DIR = 'bin' @@ -64,59 +63,58 @@ def install_environment( helpers.assert_no_additional_deps('dotnet', additional_dependencies) envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) - with clean_path_on_failure(envdir): - build_dir = 'pre-commit-build' - - # Build & pack nupkg file - helpers.run_setup_cmd( - prefix, - ( - 'dotnet', 'pack', - '--configuration', 'Release', - '--output', build_dir, - ), - ) - - nupkg_dir = prefix.path(build_dir) - nupkgs = [x for x in os.listdir(nupkg_dir) if x.endswith('.nupkg')] - - if not nupkgs: - raise AssertionError('could not find any build outputs to install') - - for nupkg in nupkgs: - with zipfile.ZipFile(os.path.join(nupkg_dir, nupkg)) as f: - nuspec, = (x for x in f.namelist() if x.endswith('.nuspec')) - with f.open(nuspec) as spec: - tree = xml.etree.ElementTree.parse(spec) - - namespace = re.match(r'{.*}', tree.getroot().tag) - if not namespace: - raise AssertionError('could not parse namespace from nuspec') - - tool_id_element = tree.find(f'.//{namespace[0]}id') - if tool_id_element is None: - raise AssertionError('expected to find an "id" element') - - tool_id = tool_id_element.text - if not tool_id: - raise AssertionError('"id" element missing tool name') - - # Install to bin dir - with _nuget_config_no_sources() as nuget_config: - helpers.run_setup_cmd( - prefix, - ( - 'dotnet', 'tool', 'install', - '--configfile', nuget_config, - '--tool-path', os.path.join(envdir, BIN_DIR), - '--add-source', build_dir, - tool_id, - ), - ) - - # Clean the git dir, ignoring the environment dir - clean_cmd = ('git', 'clean', '-ffxd', '-e', f'{ENVIRONMENT_DIR}-*') - helpers.run_setup_cmd(prefix, clean_cmd) + build_dir = 'pre-commit-build' + + # Build & pack nupkg file + helpers.run_setup_cmd( + prefix, + ( + 'dotnet', 'pack', + '--configuration', 'Release', + '--output', build_dir, + ), + ) + + nupkg_dir = prefix.path(build_dir) + nupkgs = [x for x in os.listdir(nupkg_dir) if x.endswith('.nupkg')] + + if not nupkgs: + raise AssertionError('could not find any build outputs to install') + + for nupkg in nupkgs: + with zipfile.ZipFile(os.path.join(nupkg_dir, nupkg)) as f: + nuspec, = (x for x in f.namelist() if x.endswith('.nuspec')) + with f.open(nuspec) as spec: + tree = xml.etree.ElementTree.parse(spec) + + namespace = re.match(r'{.*}', tree.getroot().tag) + if not namespace: + raise AssertionError('could not parse namespace from nuspec') + + tool_id_element = tree.find(f'.//{namespace[0]}id') + if tool_id_element is None: + raise AssertionError('expected to find an "id" element') + + tool_id = tool_id_element.text + if not tool_id: + raise AssertionError('"id" element missing tool name') + + # Install to bin dir + with _nuget_config_no_sources() as nuget_config: + helpers.run_setup_cmd( + prefix, + ( + 'dotnet', 'tool', 'install', + '--configfile', nuget_config, + '--tool-path', os.path.join(envdir, BIN_DIR), + '--add-source', build_dir, + tool_id, + ), + ) + + # Clean the git dir, ignoring the environment dir + clean_cmd = ('git', 'clean', '-ffxd', '-e', f'{ENVIRONMENT_DIR}-*') + helpers.run_setup_cmd(prefix, clean_cmd) def run_hook( diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index a5f9dba02..36792393a 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -14,7 +14,6 @@ from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix -from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b from pre_commit.util import rmtree @@ -65,31 +64,30 @@ def install_environment( helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), ) - with clean_path_on_failure(directory): - remote = git.get_remote_url(prefix.prefix_dir) - repo_src_dir = os.path.join(directory, 'src', guess_go_dir(remote)) - - # Clone into the goenv we'll create - cmd = ('git', 'clone', '--recursive', '.', repo_src_dir) - helpers.run_setup_cmd(prefix, cmd) - - if sys.platform == 'cygwin': # pragma: no cover - _, gopath, _ = cmd_output('cygpath', '-w', directory) - gopath = gopath.strip() - else: - gopath = directory - env = dict(os.environ, GOPATH=gopath) - env.pop('GOBIN', None) - cmd_output_b('go', 'install', './...', cwd=repo_src_dir, env=env) - for dependency in additional_dependencies: - cmd_output_b( - 'go', 'install', dependency, cwd=repo_src_dir, env=env, - ) - # Same some disk space, we don't need these after installation - rmtree(prefix.path(directory, 'src')) - pkgdir = prefix.path(directory, 'pkg') - if os.path.exists(pkgdir): # pragma: no cover (go<1.10) - rmtree(pkgdir) + remote = git.get_remote_url(prefix.prefix_dir) + repo_src_dir = os.path.join(directory, 'src', guess_go_dir(remote)) + + # Clone into the goenv we'll create + cmd = ('git', 'clone', '--recursive', '.', repo_src_dir) + helpers.run_setup_cmd(prefix, cmd) + + if sys.platform == 'cygwin': # pragma: no cover + _, gopath, _ = cmd_output('cygpath', '-w', directory) + gopath = gopath.strip() + else: + gopath = directory + env = dict(os.environ, GOPATH=gopath) + env.pop('GOBIN', None) + cmd_output_b('go', 'install', './...', cwd=repo_src_dir, env=env) + for dependency in additional_dependencies: + cmd_output_b( + 'go', 'install', dependency, cwd=repo_src_dir, env=env, + ) + # Same some disk space, we don't need these after installation + rmtree(prefix.path(directory, 'src')) + pkgdir = prefix.path(directory, 'pkg') + if os.path.exists(pkgdir): # pragma: no cover (go<1.10) + rmtree(pkgdir) def run_hook( diff --git a/pre_commit/languages/lua.py b/pre_commit/languages/lua.py index 49aa7308c..cd38a2974 100644 --- a/pre_commit/languages/lua.py +++ b/pre_commit/languages/lua.py @@ -13,7 +13,6 @@ from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix -from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output ENVIRONMENT_DIR = 'lua_env' @@ -64,22 +63,21 @@ def install_environment( helpers.assert_version_default('lua', version) envdir = _envdir(prefix) - with clean_path_on_failure(envdir): - with in_env(prefix): - # luarocks doesn't bootstrap a tree prior to installing - # so ensure the directory exists. - os.makedirs(envdir, exist_ok=True) - - # Older luarocks (e.g., 2.4.2) expect the rockspec as an arg - for rockspec in prefix.star('.rockspec'): - make_cmd = ('luarocks', '--tree', envdir, 'make', rockspec) - helpers.run_setup_cmd(prefix, make_cmd) - - # luarocks can't install multiple packages at once - # so install them individually. - for dependency in additional_dependencies: - cmd = ('luarocks', '--tree', envdir, 'install', dependency) - helpers.run_setup_cmd(prefix, cmd) + with in_env(prefix): + # luarocks doesn't bootstrap a tree prior to installing + # so ensure the directory exists. + os.makedirs(envdir, exist_ok=True) + + # Older luarocks (e.g., 2.4.2) expect the rockspec as an arg + for rockspec in prefix.star('.rockspec'): + make_cmd = ('luarocks', '--tree', envdir, 'make', rockspec) + helpers.run_setup_cmd(prefix, make_cmd) + + # luarocks can't install multiple packages at once + # so install them individually. + for dependency in additional_dependencies: + cmd = ('luarocks', '--tree', envdir, 'install', dependency) + helpers.run_setup_cmd(prefix, cmd) def run_hook( diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 37a5b63f1..353fa1522 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -16,7 +16,6 @@ from pre_commit.languages import helpers from pre_commit.languages.python import bin_dir from pre_commit.prefix import Prefix -from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b from pre_commit.util import rmtree @@ -85,41 +84,37 @@ def health_check(prefix: Prefix, language_version: str) -> str | None: def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: - additional_dependencies = tuple(additional_dependencies) assert prefix.exists('package.json') envdir = _envdir(prefix, version) # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx?f=255&MSPPError=-2147217396#maxpath if sys.platform == 'win32': # pragma: no cover envdir = fr'\\?\{os.path.normpath(envdir)}' - with clean_path_on_failure(envdir): - cmd = [ - sys.executable, '-mnodeenv', '--prebuilt', '--clean-src', envdir, - ] - if version != C.DEFAULT: - cmd.extend(['-n', version]) - cmd_output_b(*cmd) - - with in_env(prefix, version): - # https://npm.community/t/npm-install-g-git-vs-git-clone-cd-npm-install-g/5449 - # install as if we installed from git - - local_install_cmd = ( - 'npm', 'install', '--dev', '--prod', - '--ignore-prepublish', '--no-progress', '--no-save', - ) - helpers.run_setup_cmd(prefix, local_install_cmd) - - _, pkg, _ = cmd_output('npm', 'pack', cwd=prefix.prefix_dir) - pkg = prefix.path(pkg.strip()) - - install = ('npm', 'install', '-g', pkg, *additional_dependencies) - helpers.run_setup_cmd(prefix, install) - - # clean these up after installation - if prefix.exists('node_modules'): # pragma: win32 no cover - rmtree(prefix.path('node_modules')) - os.remove(pkg) + cmd = [sys.executable, '-mnodeenv', '--prebuilt', '--clean-src', envdir] + if version != C.DEFAULT: + cmd.extend(['-n', version]) + cmd_output_b(*cmd) + + with in_env(prefix, version): + # https://npm.community/t/npm-install-g-git-vs-git-clone-cd-npm-install-g/5449 + # install as if we installed from git + + local_install_cmd = ( + 'npm', 'install', '--dev', '--prod', + '--ignore-prepublish', '--no-progress', '--no-save', + ) + helpers.run_setup_cmd(prefix, local_install_cmd) + + _, pkg, _ = cmd_output('npm', 'pack', cwd=prefix.prefix_dir) + pkg = prefix.path(pkg.strip()) + + install = ('npm', 'install', '-g', pkg, *additional_dependencies) + helpers.run_setup_cmd(prefix, install) + + # clean these up after installation + if prefix.exists('node_modules'): # pragma: win32 no cover + rmtree(prefix.path('node_modules')) + os.remove(pkg) def run_hook( diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py index 78bd65a2b..25c016766 100644 --- a/pre_commit/languages/perl.py +++ b/pre_commit/languages/perl.py @@ -12,7 +12,6 @@ from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix -from pre_commit.util import clean_path_on_failure ENVIRONMENT_DIR = 'perl_env' get_default_version = helpers.basic_get_default_version @@ -52,11 +51,10 @@ def install_environment( ) -> None: helpers.assert_version_default('perl', version) - with clean_path_on_failure(_envdir(prefix, version)): - with in_env(prefix, version): - helpers.run_setup_cmd( - prefix, ('cpan', '-T', '.', *additional_dependencies), - ) + with in_env(prefix, version): + helpers.run_setup_cmd( + prefix, ('cpan', '-T', '.', *additional_dependencies), + ) def run_hook( diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 19fa247ef..6770499de 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -17,7 +17,6 @@ from pre_commit.parse_shebang import find_executable from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError -from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b from pre_commit.util import win_exe @@ -215,10 +214,9 @@ def install_environment( venv_cmd.extend(('-p', python)) install_cmd = ('python', '-mpip', 'install', '.', *additional_dependencies) - with clean_path_on_failure(envdir): - cmd_output_b(*venv_cmd, cwd='/') - with in_env(prefix, version): - helpers.run_setup_cmd(prefix, install_cmd) + cmd_output_b(*venv_cmd, cwd='/') + with in_env(prefix, version): + helpers.run_setup_cmd(prefix, install_cmd) def run_hook( diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index c050d451b..9bbfdbe2c 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -13,7 +13,6 @@ from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix -from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b from pre_commit.util import win_exe @@ -95,54 +94,53 @@ def install_environment( additional_dependencies: Sequence[str], ) -> None: env_dir = _get_env_dir(prefix, version) - with clean_path_on_failure(env_dir): - os.makedirs(env_dir, exist_ok=True) - shutil.copy(prefix.path('renv.lock'), env_dir) - shutil.copytree(prefix.path('renv'), os.path.join(env_dir, 'renv')) - - r_code_inst_environment = f"""\ - prefix_dir <- {prefix.prefix_dir!r} - options( - repos = c(CRAN = "https://cran.rstudio.com"), - renv.consent = TRUE - ) - source("renv/activate.R") - renv::restore() - activate_statement <- paste0( - 'suppressWarnings({{', - 'old <- setwd("', getwd(), '"); ', - 'source("renv/activate.R"); ', - 'setwd(old); ', - 'renv::load("', getwd(), '");}})' - ) - writeLines(activate_statement, 'activate.R') - is_package <- tryCatch( - {{ - path_desc <- file.path(prefix_dir, 'DESCRIPTION') - suppressWarnings(desc <- read.dcf(path_desc)) - "Package" %in% colnames(desc) - }}, - error = function(...) FALSE - ) - if (is_package) {{ - renv::install(prefix_dir) - }} - """ - - cmd_output_b( - _rscript_exec(), '--vanilla', '-e', - _inline_r_setup(r_code_inst_environment), - cwd=env_dir, + os.makedirs(env_dir, exist_ok=True) + shutil.copy(prefix.path('renv.lock'), env_dir) + shutil.copytree(prefix.path('renv'), os.path.join(env_dir, 'renv')) + + r_code_inst_environment = f"""\ + prefix_dir <- {prefix.prefix_dir!r} + options( + repos = c(CRAN = "https://cran.rstudio.com"), + renv.consent = TRUE + ) + source("renv/activate.R") + renv::restore() + activate_statement <- paste0( + 'suppressWarnings({{', + 'old <- setwd("', getwd(), '"); ', + 'source("renv/activate.R"); ', + 'setwd(old); ', + 'renv::load("', getwd(), '");}})' ) - if additional_dependencies: - r_code_inst_add = 'renv::install(commandArgs(trailingOnly = TRUE))' - with in_env(prefix, version): - cmd_output_b( - _rscript_exec(), *RSCRIPT_OPTS, '-e', - _inline_r_setup(r_code_inst_add), - *additional_dependencies, - cwd=env_dir, - ) + writeLines(activate_statement, 'activate.R') + is_package <- tryCatch( + {{ + path_desc <- file.path(prefix_dir, 'DESCRIPTION') + suppressWarnings(desc <- read.dcf(path_desc)) + "Package" %in% colnames(desc) + }}, + error = function(...) FALSE + ) + if (is_package) {{ + renv::install(prefix_dir) + }} + """ + + cmd_output_b( + _rscript_exec(), '--vanilla', '-e', + _inline_r_setup(r_code_inst_environment), + cwd=env_dir, + ) + if additional_dependencies: + r_code_inst_add = 'renv::install(commandArgs(trailingOnly = TRUE))' + with in_env(prefix, version): + cmd_output_b( + _rscript_exec(), *RSCRIPT_OPTS, '-e', + _inline_r_setup(r_code_inst_add), + *additional_dependencies, + cwd=env_dir, + ) def _inline_r_setup(code: str) -> str: diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 8955dd011..379427b02 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -17,7 +17,6 @@ from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError -from pre_commit.util import clean_path_on_failure from pre_commit.util import resource_bytesio ENVIRONMENT_DIR = 'rbenv' @@ -115,33 +114,30 @@ def _install_ruby( def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: - additional_dependencies = tuple(additional_dependencies) - directory = helpers.environment_dir(ENVIRONMENT_DIR, version) - with clean_path_on_failure(prefix.path(directory)): - if version != 'system': # pragma: win32 no cover - _install_rbenv(prefix, version) - with in_env(prefix, version): - # Need to call this before installing so rbenv's directories - # are set up - helpers.run_setup_cmd(prefix, ('rbenv', 'init', '-')) - if version != C.DEFAULT: - _install_ruby(prefix, version) - # Need to call this after installing to set up the shims - helpers.run_setup_cmd(prefix, ('rbenv', 'rehash')) - + if version != 'system': # pragma: win32 no cover + _install_rbenv(prefix, version) with in_env(prefix, version): - helpers.run_setup_cmd( - prefix, ('gem', 'build', *prefix.star('.gemspec')), - ) - helpers.run_setup_cmd( - prefix, - ( - 'gem', 'install', - '--no-document', '--no-format-executable', - '--no-user-install', - *prefix.star('.gem'), *additional_dependencies, - ), - ) + # Need to call this before installing so rbenv's directories + # are set up + helpers.run_setup_cmd(prefix, ('rbenv', 'init', '-')) + if version != C.DEFAULT: + _install_ruby(prefix, version) + # Need to call this after installing to set up the shims + helpers.run_setup_cmd(prefix, ('rbenv', 'rehash')) + + with in_env(prefix, version): + helpers.run_setup_cmd( + prefix, ('gem', 'build', *prefix.star('.gemspec')), + ) + helpers.run_setup_cmd( + prefix, + ( + 'gem', 'install', + '--no-document', '--no-format-executable', + '--no-user-install', + *prefix.star('.gem'), *additional_dependencies, + ), + ) def run_hook( diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 204f2aa79..67e7ae85c 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -18,7 +18,6 @@ from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix -from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b from pre_commit.util import make_executable from pre_commit.util import win_exe @@ -143,28 +142,27 @@ def install_environment( } lib_deps = set(additional_dependencies) - cli_deps - with clean_path_on_failure(directory): - packages_to_install: set[tuple[str, ...]] = {('--path', '.')} - for cli_dep in cli_deps: - cli_dep = cli_dep[len('cli:'):] - package, _, crate_version = cli_dep.partition(':') - if crate_version != '': - packages_to_install.add((package, '--version', crate_version)) - else: - packages_to_install.add((package,)) + packages_to_install: set[tuple[str, ...]] = {('--path', '.')} + for cli_dep in cli_deps: + cli_dep = cli_dep[len('cli:'):] + package, _, crate_version = cli_dep.partition(':') + if crate_version != '': + packages_to_install.add((package, '--version', crate_version)) + else: + packages_to_install.add((package,)) - with in_env(prefix, version): - if version != 'system': - install_rust_with_toolchain(_rust_toolchain(version)) + with in_env(prefix, version): + if version != 'system': + install_rust_with_toolchain(_rust_toolchain(version)) - if len(lib_deps) > 0: - _add_dependencies(prefix, lib_deps) + if len(lib_deps) > 0: + _add_dependencies(prefix, lib_deps) - for args in packages_to_install: - cmd_output_b( - 'cargo', 'install', '--bins', '--root', directory, *args, - cwd=prefix.prefix_dir, - ) + for args in packages_to_install: + cmd_output_b( + 'cargo', 'install', '--bins', '--root', directory, *args, + cwd=prefix.prefix_dir, + ) def run_hook( diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 4c687030c..0fab596cc 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -12,7 +12,6 @@ from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix -from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'swift_env' @@ -46,14 +45,13 @@ def install_environment( ) # Build the swift package - with clean_path_on_failure(directory): - os.mkdir(directory) - cmd_output_b( - 'swift', 'build', - '-C', prefix.prefix_dir, - '-c', BUILD_CONFIG, - '--build-path', os.path.join(directory, BUILD_DIR), - ) + os.mkdir(directory) + cmd_output_b( + 'swift', 'build', + '-C', prefix.prefix_dir, + '-c', BUILD_CONFIG, + '--build-path', os.path.join(directory, BUILD_DIR), + ) def run_hook( diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 50bc64552..fa5322dc1 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -16,6 +16,7 @@ from pre_commit.languages.helpers import environment_dir from pre_commit.prefix import Prefix from pre_commit.store import Store +from pre_commit.util import clean_path_on_failure from pre_commit.util import rmtree @@ -26,12 +27,12 @@ def _state(additional_deps: Sequence[str]) -> object: return {'additional_dependencies': sorted(additional_deps)} -def _state_filename(prefix: Prefix, venv: str) -> str: - return prefix.path(venv, f'.install_state_v{C.INSTALLED_STATE_VERSION}') +def _state_filename(venv: str) -> str: + return os.path.join(venv, f'.install_state_v{C.INSTALLED_STATE_VERSION}') -def _read_state(prefix: Prefix, venv: str) -> object | None: - filename = _state_filename(prefix, venv) +def _read_state(venv: str) -> object | None: + filename = _state_filename(venv) if not os.path.exists(filename): return None else: @@ -39,26 +40,15 @@ def _read_state(prefix: Prefix, venv: str) -> object | None: return json.load(f) -def _write_state(prefix: Prefix, venv: str, state: object) -> None: - state_filename = _state_filename(prefix, venv) - staging = f'{state_filename}staging' - with open(staging, 'w') as state_file: - state_file.write(json.dumps(state)) - # Move the file into place atomically to indicate we've installed - os.replace(staging, state_filename) - - def _hook_installed(hook: Hook) -> bool: lang = languages[hook.language] if lang.ENVIRONMENT_DIR is None: return True venv = environment_dir(lang.ENVIRONMENT_DIR, hook.language_version) + venv = hook.prefix.path(venv) return ( - ( - _read_state(hook.prefix, venv) == - _state(hook.additional_dependencies) - ) and + _read_state(venv) == _state(hook.additional_dependencies) and not lang.health_check(hook.prefix, hook.language_version) ) @@ -70,26 +60,34 @@ def _hook_install(hook: Hook) -> None: lang = languages[hook.language] assert lang.ENVIRONMENT_DIR is not None + venv = environment_dir(lang.ENVIRONMENT_DIR, hook.language_version) + venv = hook.prefix.path(venv) # There's potentially incomplete cleanup from previous runs # Clean it up! - if hook.prefix.exists(venv): - rmtree(hook.prefix.path(venv)) + if os.path.exists(venv): + rmtree(venv) - lang.install_environment( - hook.prefix, hook.language_version, hook.additional_dependencies, - ) - health_error = lang.health_check(hook.prefix, hook.language_version) - if health_error: - raise AssertionError( - f'BUG: expected environment for {hook.language} to be healthy ' - f'immediately after install, please open an issue describing ' - f'your environment\n\n' - f'more info:\n\n{health_error}', + with clean_path_on_failure(venv): + lang.install_environment( + hook.prefix, hook.language_version, hook.additional_dependencies, ) - # Write our state to indicate we're installed - _write_state(hook.prefix, venv, _state(hook.additional_dependencies)) + health_error = lang.health_check(hook.prefix, hook.language_version) + if health_error: + raise AssertionError( + f'BUG: expected environment for {hook.language} to be healthy ' + f'immediately after install, please open an issue describing ' + f'your environment\n\n' + f'more info:\n\n{health_error}', + ) + # Write our state to indicate we're installed + state_filename = _state_filename(venv) + staging = f'{state_filename}staging' + with open(staging, 'w') as state_file: + state_file.write(json.dumps(_state(hook.additional_dependencies))) + # Move the file into place atomically to indicate we've installed + os.replace(staging, state_filename) def _hook( From 05c8911363a84ec062c2ccfde6d1279f0b5634b3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 1 Jan 2023 21:11:56 -0500 Subject: [PATCH 714/967] simplify environment_dir --- pre_commit/languages/conda.py | 6 ++--- pre_commit/languages/coursier.py | 11 ++++---- pre_commit/languages/dart.py | 5 ++-- pre_commit/languages/docker.py | 4 +-- pre_commit/languages/dotnet.py | 5 ++-- pre_commit/languages/golang.py | 8 ++---- pre_commit/languages/helpers.py | 4 +-- pre_commit/languages/lua.py | 10 +++----- pre_commit/languages/node.py | 10 +++----- pre_commit/languages/perl.py | 8 ++---- pre_commit/languages/python.py | 8 +++--- pre_commit/languages/r.py | 8 ++---- pre_commit/languages/ruby.py | 10 +++----- pre_commit/languages/rust.py | 14 +++-------- pre_commit/languages/swift.py | 12 +++------ pre_commit/repository.py | 14 ++++++++--- tests/repository_test.py | 43 +++++++++++++++++++------------- 17 files changed, 77 insertions(+), 103 deletions(-) diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index 76ae0781f..5a0a720f3 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -44,8 +44,7 @@ def in_env( prefix: Prefix, language_version: str, ) -> Generator[None, None, None]: - directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version) - envdir = prefix.path(directory) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) with envcontext(get_env_patch(envdir)): yield @@ -65,11 +64,10 @@ def install_environment( additional_dependencies: Sequence[str], ) -> None: helpers.assert_version_default('conda', version) - directory = helpers.environment_dir(ENVIRONMENT_DIR, version) conda_exe = _conda_exe() - env_dir = prefix.path(directory) + env_dir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) cmd_output_b( conda_exe, 'env', 'create', '-p', env_dir, '--file', 'environment.yml', cwd=prefix.prefix_dir, diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index 0d520f0fb..fdea3cd71 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -35,7 +35,7 @@ def install_environment( 'executables in the application search path', ) - envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) channel = prefix.path('.pre-commit-channel') for app_descriptor in os.listdir(channel): _, app_file = os.path.split(app_descriptor) @@ -62,11 +62,10 @@ def get_env_patch(target_dir: str) -> PatchesT: # pragma: win32 no cover @contextlib.contextmanager def in_env( prefix: Prefix, + language_version: str, ) -> Generator[None, None, None]: # pragma: win32 no cover - target_dir = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, get_default_version()), - ) - with envcontext(get_env_patch(target_dir)): + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) + with envcontext(get_env_patch(envdir)): yield @@ -75,5 +74,5 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: # pragma: win32 no cover - with in_env(hook.prefix): + with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py index 73fffdb86..9fbb63cc0 100644 --- a/pre_commit/languages/dart.py +++ b/pre_commit/languages/dart.py @@ -31,8 +31,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix) -> Generator[None, None, None]: - directory = helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT) - envdir = prefix.path(directory) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) with envcontext(get_env_patch(envdir)): yield @@ -44,7 +43,7 @@ def install_environment( ) -> None: helpers.assert_version_default('dart', version) - envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) bin_dir = os.path.join(envdir, 'bin') def _install_dir(prefix_p: Prefix, pub_cache: str) -> None: diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 5d6146740..c51cf7c10 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -94,9 +94,7 @@ def install_environment( helpers.assert_version_default('docker', version) helpers.assert_no_additional_deps('docker', additional_dependencies) - directory = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), - ) + directory = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) # Docker doesn't really have relevant disk environment, but pre-commit # still needs to cleanup its state files on failure diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index d748c8131..0bb0210cd 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -32,8 +32,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix) -> Generator[None, None, None]: - directory = helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT) - envdir = prefix.path(directory) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) with envcontext(get_env_patch(envdir)): yield @@ -62,7 +61,7 @@ def install_environment( helpers.assert_version_default('dotnet', version) helpers.assert_no_additional_deps('dotnet', additional_dependencies) - envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) build_dir = 'pre-commit-build' # Build & pack nupkg file diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 36792393a..70f0e65d4 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -31,9 +31,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix) -> Generator[None, None, None]: - envdir = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), - ) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) with envcontext(get_env_patch(envdir)): yield @@ -60,9 +58,7 @@ def install_environment( additional_dependencies: Sequence[str], ) -> None: helpers.assert_version_default('golang', version) - directory = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), - ) + directory = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) remote = git.get_remote_url(prefix.prefix_dir) repo_src_dir = os.path.join(directory, 'src', guess_go_dir(remote)) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index d462e86ca..098e95c5a 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -47,8 +47,8 @@ def run_setup_cmd(prefix: Prefix, cmd: tuple[str, ...], **kwargs: Any) -> None: cmd_output_b(*cmd, cwd=prefix.prefix_dir, **kwargs) -def environment_dir(d: str, language_version: str) -> str: - return f'{d}-{language_version}' +def environment_dir(prefix: Prefix, d: str, language_version: str) -> str: + return prefix.path(f'{d}-{language_version}') def assert_version_default(binary: str, version: str) -> None: diff --git a/pre_commit/languages/lua.py b/pre_commit/languages/lua.py index cd38a2974..26c8f1b7d 100644 --- a/pre_commit/languages/lua.py +++ b/pre_commit/languages/lua.py @@ -44,14 +44,10 @@ def get_env_patch(d: str) -> PatchesT: # pragma: win32 no cover ) -def _envdir(prefix: Prefix) -> str: # pragma: win32 no cover - directory = helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT) - return prefix.path(directory) - - @contextlib.contextmanager # pragma: win32 no cover def in_env(prefix: Prefix) -> Generator[None, None, None]: - with envcontext(get_env_patch(_envdir(prefix))): + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) + with envcontext(get_env_patch(envdir)): yield @@ -62,7 +58,7 @@ def install_environment( ) -> None: # pragma: win32 no cover helpers.assert_version_default('lua', version) - envdir = _envdir(prefix) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) with in_env(prefix): # luarocks doesn't bootstrap a tree prior to installing # so ensure the directory exists. diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 353fa1522..8facfe007 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -36,11 +36,6 @@ def get_default_version() -> str: return C.DEFAULT -def _envdir(prefix: Prefix, version: str) -> str: - directory = helpers.environment_dir(ENVIRONMENT_DIR, version) - return prefix.path(directory) - - def get_env_patch(venv: str) -> PatchesT: if sys.platform == 'cygwin': # pragma: no cover _, win_venv, _ = cmd_output('cygpath', '-w', venv) @@ -68,7 +63,8 @@ def in_env( prefix: Prefix, language_version: str, ) -> Generator[None, None, None]: - with envcontext(get_env_patch(_envdir(prefix, language_version))): + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) + with envcontext(get_env_patch(envdir)): yield @@ -85,7 +81,7 @@ def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: assert prefix.exists('package.json') - envdir = _envdir(prefix, version) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx?f=255&MSPPError=-2147217396#maxpath if sys.platform == 'win32': # pragma: no cover diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py index 25c016766..95be65599 100644 --- a/pre_commit/languages/perl.py +++ b/pre_commit/languages/perl.py @@ -18,11 +18,6 @@ health_check = helpers.basic_health_check -def _envdir(prefix: Prefix, version: str) -> str: - directory = helpers.environment_dir(ENVIRONMENT_DIR, version) - return prefix.path(directory) - - def get_env_patch(venv: str) -> PatchesT: return ( ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), @@ -42,7 +37,8 @@ def in_env( prefix: Prefix, language_version: str, ) -> Generator[None, None, None]: - with envcontext(get_env_patch(_envdir(prefix, language_version))): + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) + with envcontext(get_env_patch(envdir)): yield diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 6770499de..a7744d642 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -156,15 +156,13 @@ def in_env( prefix: Prefix, language_version: str, ) -> Generator[None, None, None]: - directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version) - envdir = prefix.path(directory) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) with envcontext(get_env_patch(envdir)): yield def health_check(prefix: Prefix, language_version: str) -> str | None: - directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version) - envdir = prefix.path(directory) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) pyvenv_cfg = os.path.join(envdir, 'pyvenv.cfg') # created with "old" virtualenv @@ -207,7 +205,7 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) venv_cmd = [sys.executable, '-mvirtualenv', envdir] python = norm_version(version) if python is not None: diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 9bbfdbe2c..d2ec83daa 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -34,15 +34,11 @@ def in_env( prefix: Prefix, language_version: str, ) -> Generator[None, None, None]: - envdir = _get_env_dir(prefix, language_version) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) with envcontext(get_env_patch(envdir)): yield -def _get_env_dir(prefix: Prefix, version: str) -> str: - return prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) - - def _prefix_if_file_entry(entry: list[str], prefix: Prefix) -> Sequence[str]: if entry[1] == '-e': return entry[1:] @@ -93,7 +89,7 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - env_dir = _get_env_dir(prefix, version) + env_dir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) os.makedirs(env_dir, exist_ok=True) shutil.copy(prefix.path('renv.lock'), env_dir) shutil.copytree(prefix.path('renv'), os.path.join(env_dir, 'renv')) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 379427b02..89af25459 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -71,9 +71,7 @@ def in_env( prefix: Prefix, language_version: str, ) -> Generator[None, None, None]: - envdir = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, language_version), - ) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) with envcontext(get_env_patch(envdir, language_version)): yield @@ -88,14 +86,14 @@ def _install_rbenv( prefix: Prefix, version: str, ) -> None: # pragma: win32 no cover - directory = helpers.environment_dir(ENVIRONMENT_DIR, version) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) _extract_resource('rbenv.tar.gz', prefix.path('.')) - shutil.move(prefix.path('rbenv'), prefix.path(directory)) + shutil.move(prefix.path('rbenv'), envdir) # Only install ruby-build if the version is specified if version != C.DEFAULT: - plugins_dir = prefix.path(directory, 'plugins') + plugins_dir = os.path.join(envdir, 'plugins') _extract_resource('ruby-download.tar.gz', plugins_dir) _extract_resource('ruby-build.tar.gz', plugins_dir) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 67e7ae85c..0f6cd332d 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -48,11 +48,6 @@ def _rust_toolchain(language_version: str) -> str: return language_version -def _envdir(prefix: Prefix, version: str) -> str: - directory = helpers.environment_dir(ENVIRONMENT_DIR, version) - return prefix.path(directory) - - def get_env_patch(target_dir: str, version: str) -> PatchesT: return ( ('CARGO_HOME', target_dir), @@ -71,9 +66,8 @@ def in_env( prefix: Prefix, language_version: str, ) -> Generator[None, None, None]: - with envcontext( - get_env_patch(_envdir(prefix, language_version), language_version), - ): + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) + with envcontext(get_env_patch(envdir, language_version)): yield @@ -125,7 +119,7 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - directory = _envdir(prefix, version) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) # There are two cases where we might want to specify more dependencies: # as dependencies for the library being built, and as binary packages @@ -160,7 +154,7 @@ def install_environment( for args in packages_to_install: cmd_output_b( - 'cargo', 'install', '--bins', '--root', directory, *args, + 'cargo', 'install', '--bins', '--root', envdir, *args, cwd=prefix.prefix_dir, ) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 0fab596cc..7cc61d958 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -28,9 +28,7 @@ def get_env_patch(venv: str) -> PatchesT: # pragma: win32 no cover @contextlib.contextmanager # pragma: win32 no cover def in_env(prefix: Prefix) -> Generator[None, None, None]: - envdir = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), - ) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) with envcontext(get_env_patch(envdir)): yield @@ -40,17 +38,15 @@ def install_environment( ) -> None: # pragma: win32 no cover helpers.assert_version_default('swift', version) helpers.assert_no_additional_deps('swift', additional_dependencies) - directory = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), - ) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) # Build the swift package - os.mkdir(directory) + os.mkdir(envdir) cmd_output_b( 'swift', 'build', '-C', prefix.prefix_dir, '-c', BUILD_CONFIG, - '--build-path', os.path.join(directory, BUILD_DIR), + '--build-path', os.path.join(envdir, BUILD_DIR), ) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index fa5322dc1..ac6b84463 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -45,8 +45,11 @@ def _hook_installed(hook: Hook) -> bool: if lang.ENVIRONMENT_DIR is None: return True - venv = environment_dir(lang.ENVIRONMENT_DIR, hook.language_version) - venv = hook.prefix.path(venv) + venv = environment_dir( + hook.prefix, + lang.ENVIRONMENT_DIR, + hook.language_version, + ) return ( _read_state(venv) == _state(hook.additional_dependencies) and not lang.health_check(hook.prefix, hook.language_version) @@ -61,8 +64,11 @@ def _hook_install(hook: Hook) -> None: lang = languages[hook.language] assert lang.ENVIRONMENT_DIR is not None - venv = environment_dir(lang.ENVIRONMENT_DIR, hook.language_version) - venv = hook.prefix.path(venv) + venv = environment_dir( + hook.prefix, + lang.ENVIRONMENT_DIR, + hook.language_version, + ) # There's potentially incomplete cleanup from previous runs # Clean it up! diff --git a/tests/repository_test.py b/tests/repository_test.py index 6aa0f0073..fa8bf4319 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -463,11 +463,12 @@ def test_additional_rust_cli_dependencies_installed( # A small rust package with no dependencies. config['hooks'][0]['additional_dependencies'] = [dep] hook = _get_hook(config, store, 'rust-hook') - binaries = os.listdir( - hook.prefix.path( - helpers.environment_dir(rust.ENVIRONMENT_DIR, 'system'), 'bin', - ), + envdir = helpers.environment_dir( + hook.prefix, + rust.ENVIRONMENT_DIR, + 'system', ) + binaries = os.listdir(os.path.join(envdir, 'bin')) # normalize for windows binaries = [os.path.splitext(binary)[0] for binary in binaries] assert 'shellharden' in binaries @@ -482,11 +483,12 @@ def test_additional_rust_lib_dependencies_installed( deps = ['shellharden:3.1.0', 'git-version'] config['hooks'][0]['additional_dependencies'] = deps hook = _get_hook(config, store, 'rust-hook') - binaries = os.listdir( - hook.prefix.path( - helpers.environment_dir(rust.ENVIRONMENT_DIR, 'system'), 'bin', - ), + envdir = helpers.environment_dir( + hook.prefix, + rust.ENVIRONMENT_DIR, + 'system', ) + binaries = os.listdir(os.path.join(envdir, 'bin')) # normalize for windows binaries = [os.path.splitext(binary)[0] for binary in binaries] assert 'rust-hello-world' in binaries @@ -672,11 +674,12 @@ def test_additional_golang_dependencies_installed( deps = ['golang.org/x/example/hello@latest'] config['hooks'][0]['additional_dependencies'] = deps hook = _get_hook(config, store, 'golang-hook') - binaries = os.listdir( - hook.prefix.path( - helpers.environment_dir(golang.ENVIRONMENT_DIR, C.DEFAULT), 'bin', - ), + envdir = helpers.environment_dir( + hook.prefix, + golang.ENVIRONMENT_DIR, + C.DEFAULT, ) + binaries = os.listdir(os.path.join(envdir, 'bin')) # normalize for windows binaries = [os.path.splitext(binary)[0] for binary in binaries] assert 'hello' in binaries @@ -792,10 +795,14 @@ class MyKeyboardInterrupt(KeyboardInterrupt): # Should have made an environment, however this environment is broken! hook, = hooks - assert hook.prefix.exists( - helpers.environment_dir(python.ENVIRONMENT_DIR, hook.language_version), + envdir = helpers.environment_dir( + hook.prefix, + python.ENVIRONMENT_DIR, + hook.language_version, ) + assert os.path.exists(envdir) + # However, it should be perfectly runnable (reinstall after botched # install) install_hook_envs(hooks, store) @@ -811,10 +818,12 @@ def test_invalidated_virtualenv(tempdir_factory, store): hook = _get_hook(config, store, 'foo') # Simulate breaking of the virtualenv - libdir = hook.prefix.path( - helpers.environment_dir(python.ENVIRONMENT_DIR, hook.language_version), - 'lib', hook.language_version, + envdir = helpers.environment_dir( + hook.prefix, + python.ENVIRONMENT_DIR, + hook.language_version, ) + libdir = os.path.join(envdir, 'lib', hook.language_version) paths = [ os.path.join(libdir, p) for p in ('site.py', 'site.pyc', '__pycache__') ] From 0920cb33ee3faf614dec5ab83dd9f99a682e6a75 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 2 Jan 2023 16:00:27 -0500 Subject: [PATCH 715/967] simplify install state the additional bookkeeping has been unnecessary since b827694520be0f39bfc0599f3680b6c08b4516cf unfortunately this will cause a rebuild of all hooks in order to be forward/backward compatible -- shrugs --- pre_commit/constants.py | 2 -- pre_commit/repository.py | 27 ++++----------------------- 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 8fc5e55db..3f03ceed9 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -5,8 +5,6 @@ CONFIG_FILE = '.pre-commit-config.yaml' MANIFEST_FILE = '.pre-commit-hooks.yaml' -# Bump when installation changes in a backwards / forwards incompatible way -INSTALLED_STATE_VERSION = '1' # Bump when modifying `empty_template` LOCAL_REPO_VERSION = '1' diff --git a/pre_commit/repository.py b/pre_commit/repository.py index ac6b84463..dfa1a2fd8 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -1,6 +1,5 @@ from __future__ import annotations -import json import logging import os from typing import Any @@ -23,21 +22,8 @@ logger = logging.getLogger('pre_commit') -def _state(additional_deps: Sequence[str]) -> object: - return {'additional_dependencies': sorted(additional_deps)} - - def _state_filename(venv: str) -> str: - return os.path.join(venv, f'.install_state_v{C.INSTALLED_STATE_VERSION}') - - -def _read_state(venv: str) -> object | None: - filename = _state_filename(venv) - if not os.path.exists(filename): - return None - else: - with open(filename) as f: - return json.load(f) + return os.path.join(venv, '.install_state_v2') def _hook_installed(hook: Hook) -> bool: @@ -51,7 +37,7 @@ def _hook_installed(hook: Hook) -> bool: hook.language_version, ) return ( - _read_state(venv) == _state(hook.additional_dependencies) and + os.path.exists(_state_filename(venv)) and not lang.health_check(hook.prefix, hook.language_version) ) @@ -87,13 +73,8 @@ def _hook_install(hook: Hook) -> None: f'your environment\n\n' f'more info:\n\n{health_error}', ) - # Write our state to indicate we're installed - state_filename = _state_filename(venv) - staging = f'{state_filename}staging' - with open(staging, 'w') as state_file: - state_file.write(json.dumps(_state(hook.additional_dependencies))) - # Move the file into place atomically to indicate we've installed - os.replace(staging, state_filename) + # touch state file to indicate we're installed + open(_state_filename(venv), 'a+').close() def _hook( From 990643c1e089a7924697b32d4f2dd57dbe37785f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 2 Jan 2023 18:39:42 -0500 Subject: [PATCH 716/967] Revert "simplify install state" --- pre_commit/constants.py | 2 ++ pre_commit/repository.py | 27 +++++++++++++++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 3f03ceed9..8fc5e55db 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -5,6 +5,8 @@ CONFIG_FILE = '.pre-commit-config.yaml' MANIFEST_FILE = '.pre-commit-hooks.yaml' +# Bump when installation changes in a backwards / forwards incompatible way +INSTALLED_STATE_VERSION = '1' # Bump when modifying `empty_template` LOCAL_REPO_VERSION = '1' diff --git a/pre_commit/repository.py b/pre_commit/repository.py index dfa1a2fd8..ac6b84463 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json import logging import os from typing import Any @@ -22,8 +23,21 @@ logger = logging.getLogger('pre_commit') +def _state(additional_deps: Sequence[str]) -> object: + return {'additional_dependencies': sorted(additional_deps)} + + def _state_filename(venv: str) -> str: - return os.path.join(venv, '.install_state_v2') + return os.path.join(venv, f'.install_state_v{C.INSTALLED_STATE_VERSION}') + + +def _read_state(venv: str) -> object | None: + filename = _state_filename(venv) + if not os.path.exists(filename): + return None + else: + with open(filename) as f: + return json.load(f) def _hook_installed(hook: Hook) -> bool: @@ -37,7 +51,7 @@ def _hook_installed(hook: Hook) -> bool: hook.language_version, ) return ( - os.path.exists(_state_filename(venv)) and + _read_state(venv) == _state(hook.additional_dependencies) and not lang.health_check(hook.prefix, hook.language_version) ) @@ -73,8 +87,13 @@ def _hook_install(hook: Hook) -> None: f'your environment\n\n' f'more info:\n\n{health_error}', ) - # touch state file to indicate we're installed - open(_state_filename(venv), 'a+').close() + # Write our state to indicate we're installed + state_filename = _state_filename(venv) + staging = f'{state_filename}staging' + with open(staging, 'w') as state_file: + state_file.write(json.dumps(_state(hook.additional_dependencies))) + # Move the file into place atomically to indicate we've installed + os.replace(staging, state_filename) def _hook( From 8529a0c1d35e422304a54cc2c01d18541287e171 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 1 Jan 2023 16:58:16 -0500 Subject: [PATCH 717/967] add pre_commit.yaml module --- pre_commit/clientlib.py | 2 +- pre_commit/commands/autoupdate.py | 4 ++-- pre_commit/commands/migrate_config.py | 2 +- pre_commit/commands/try_repo.py | 2 +- pre_commit/languages/dart.py | 2 +- pre_commit/util.py | 15 --------------- pre_commit/yaml.py | 18 ++++++++++++++++++ testing/fixtures.py | 4 ++-- tests/commands/autoupdate_test.py | 5 ++--- 9 files changed, 28 insertions(+), 26 deletions(-) create mode 100644 pre_commit/yaml.py diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index e03d5d666..e191d3a00 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -14,7 +14,7 @@ import pre_commit.constants as C from pre_commit.errors import FatalError from pre_commit.languages.all import all_languages -from pre_commit.util import yaml_load +from pre_commit.yaml import yaml_load logger = logging.getLogger('pre_commit') diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 6da53112e..7ed6e7761 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -20,8 +20,8 @@ from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b -from pre_commit.util import yaml_dump -from pre_commit.util import yaml_load +from pre_commit.yaml import yaml_dump +from pre_commit.yaml import yaml_load class RevInfo(NamedTuple): diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index c3d0a509f..836936b25 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -5,7 +5,7 @@ import yaml -from pre_commit.util import yaml_load +from pre_commit.yaml import yaml_load def _is_header_line(line: str) -> bool: diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index 5244aeff4..539ed3c2b 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -12,8 +12,8 @@ from pre_commit.commands.run import run from pre_commit.store import Store from pre_commit.util import cmd_output_b -from pre_commit.util import yaml_dump from pre_commit.xargs import xargs +from pre_commit.yaml import yaml_dump logger = logging.getLogger(__name__) diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py index 9fbb63cc0..223567a54 100644 --- a/pre_commit/languages/dart.py +++ b/pre_commit/languages/dart.py @@ -15,7 +15,7 @@ from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import win_exe -from pre_commit.util import yaml_load +from pre_commit.yaml import yaml_load ENVIRONMENT_DIR = 'dartenv' diff --git a/pre_commit/util.py b/pre_commit/util.py index 324544c7e..d51fd32d9 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -2,7 +2,6 @@ import contextlib import errno -import functools import importlib.resources import os.path import shutil @@ -15,22 +14,8 @@ from typing import Generator from typing import IO -import yaml - from pre_commit import parse_shebang -Loader = getattr(yaml, 'CSafeLoader', yaml.SafeLoader) -yaml_load = functools.partial(yaml.load, Loader=Loader) -Dumper = getattr(yaml, 'CSafeDumper', yaml.SafeDumper) - - -def yaml_dump(o: Any, **kwargs: Any) -> str: - # when python/mypy#1484 is solved, this can be `functools.partial` - return yaml.dump( - o, Dumper=Dumper, default_flow_style=False, indent=4, sort_keys=False, - **kwargs, - ) - def force_bytes(exc: Any) -> bytes: with contextlib.suppress(TypeError): diff --git a/pre_commit/yaml.py b/pre_commit/yaml.py new file mode 100644 index 000000000..bdf4ec47d --- /dev/null +++ b/pre_commit/yaml.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +import functools +from typing import Any + +import yaml + +Loader = getattr(yaml, 'CSafeLoader', yaml.SafeLoader) +yaml_load = functools.partial(yaml.load, Loader=Loader) +Dumper = getattr(yaml, 'CSafeDumper', yaml.SafeDumper) + + +def yaml_dump(o: Any, **kwargs: Any) -> str: + # when python/mypy#1484 is solved, this can be `functools.partial` + return yaml.dump( + o, Dumper=Dumper, default_flow_style=False, indent=4, sort_keys=False, + **kwargs, + ) diff --git a/testing/fixtures.py b/testing/fixtures.py index 5182a083e..79a11605e 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -12,8 +12,8 @@ from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest from pre_commit.util import cmd_output -from pre_commit.util import yaml_dump -from pre_commit.util import yaml_load +from pre_commit.yaml import yaml_dump +from pre_commit.yaml import yaml_load from testing.util import get_resource_path from testing.util import git_commit diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 3806b0e48..4bcb5d82a 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -4,12 +4,11 @@ from unittest import mock import pytest -import yaml import pre_commit.constants as C from pre_commit import envcontext from pre_commit import git -from pre_commit import util +from pre_commit import yaml from pre_commit.commands.autoupdate import _check_hooks_still_exist_at_rev from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.autoupdate import RepositoryCannotBeUpdatedError @@ -206,7 +205,7 @@ def test_autoupdate_with_core_useBuiltinFSMonitor(out_of_date, tmpdir, store): def test_autoupdate_pure_yaml(out_of_date, tmpdir, store): - with mock.patch.object(util, 'Dumper', yaml.SafeDumper): + with mock.patch.object(yaml, 'Dumper', yaml.yaml.SafeDumper): test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store) From 60a42e94195492fa27e869e5034f296989cfc4a7 Mon Sep 17 00:00:00 2001 From: taoufik07 Date: Mon, 2 Jan 2023 21:14:50 +0100 Subject: [PATCH 718/967] Remove `GOPATH` special build --- pre_commit/git.py | 5 ---- pre_commit/languages/golang.py | 47 ++++++++-------------------------- tests/languages/golang_test.py | 22 ---------------- 3 files changed, 10 insertions(+), 64 deletions(-) delete mode 100644 tests/languages/golang_test.py diff --git a/pre_commit/git.py b/pre_commit/git.py index a76118f0b..333dc7ba3 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -93,11 +93,6 @@ def get_git_common_dir(git_root: str = '.') -> str: return get_git_dir(git_root) -def get_remote_url(git_root: str) -> str: - _, out, _ = cmd_output('git', 'config', 'remote.origin.url', cwd=git_root) - return out.strip() - - def is_in_merge_conflict() -> bool: git_dir = get_git_dir('.') return ( diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 70f0e65d4..a57c38dcd 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -7,7 +7,6 @@ from typing import Sequence import pre_commit.constants as C -from pre_commit import git from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var @@ -15,7 +14,6 @@ from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output -from pre_commit.util import cmd_output_b from pre_commit.util import rmtree ENVIRONMENT_DIR = 'golangenv' @@ -36,53 +34,28 @@ def in_env(prefix: Prefix) -> Generator[None, None, None]: yield -def guess_go_dir(remote_url: str) -> str: - if remote_url.endswith('.git'): - remote_url = remote_url[:-1 * len('.git')] - looks_like_url = ( - not remote_url.startswith('file://') and - ('//' in remote_url or '@' in remote_url) - ) - remote_url = remote_url.replace(':', '/') - if looks_like_url: - _, _, remote_url = remote_url.rpartition('//') - _, _, remote_url = remote_url.rpartition('@') - return remote_url - else: - return 'unknown_src_dir' - - def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: helpers.assert_version_default('golang', version) - directory = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) - - remote = git.get_remote_url(prefix.prefix_dir) - repo_src_dir = os.path.join(directory, 'src', guess_go_dir(remote)) - - # Clone into the goenv we'll create - cmd = ('git', 'clone', '--recursive', '.', repo_src_dir) - helpers.run_setup_cmd(prefix, cmd) + env_dir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) if sys.platform == 'cygwin': # pragma: no cover - _, gopath, _ = cmd_output('cygpath', '-w', directory) - gopath = gopath.strip() + gopath = cmd_output('cygpath', '-w', env_dir)[1].strip() else: - gopath = directory + gopath = env_dir env = dict(os.environ, GOPATH=gopath) env.pop('GOBIN', None) - cmd_output_b('go', 'install', './...', cwd=repo_src_dir, env=env) + + helpers.run_setup_cmd(prefix, ('go', 'install', './...'), env=env) for dependency in additional_dependencies: - cmd_output_b( - 'go', 'install', dependency, cwd=repo_src_dir, env=env, - ) - # Same some disk space, we don't need these after installation - rmtree(prefix.path(directory, 'src')) - pkgdir = prefix.path(directory, 'pkg') - if os.path.exists(pkgdir): # pragma: no cover (go<1.10) + helpers.run_setup_cmd(prefix, ('go', 'install', dependency), env=env) + + # save some disk space -- we don't need this after installation + pkgdir = os.path.join(env_dir, 'pkg') + if os.path.exists(pkgdir): # pragma: no branch (always true on windows?) rmtree(pkgdir) diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py deleted file mode 100644 index 9e393cb39..000000000 --- a/tests/languages/golang_test.py +++ /dev/null @@ -1,22 +0,0 @@ -from __future__ import annotations - -import pytest - -from pre_commit.languages.golang import guess_go_dir - - -@pytest.mark.parametrize( - ('url', 'expected'), - ( - ('/im/a/path/on/disk', 'unknown_src_dir'), - ('file:///im/a/path/on/disk', 'unknown_src_dir'), - ('git@github.com:golang/lint', 'github.com/golang/lint'), - ('git://github.com/golang/lint', 'github.com/golang/lint'), - ('http://github.com/golang/lint', 'github.com/golang/lint'), - ('https://github.com/golang/lint', 'github.com/golang/lint'), - ('ssh://git@github.com/golang/lint', 'github.com/golang/lint'), - ('git@github.com:golang/lint.git', 'github.com/golang/lint'), - ), -) -def test_guess_go_dir(url, expected): - assert guess_go_dir(url) == expected From bf1a1fa5fd6de3633b033847964b51a60ffbd0d5 Mon Sep 17 00:00:00 2001 From: taoufik07 Date: Thu, 5 Jan 2023 13:31:28 +0100 Subject: [PATCH 719/967] Fix command normalization when a custom env is passed --- pre_commit/parse_shebang.py | 18 +++++++++------ pre_commit/util.py | 2 +- tests/commands/install_uninstall_test.py | 29 ++++++++++++------------ tests/languages/rust_test.py | 7 ++++-- tests/parse_shebang_test.py | 6 ++--- 5 files changed, 35 insertions(+), 27 deletions(-) diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index 3ac933c09..3ee04e8d7 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -20,13 +20,13 @@ def parse_filename(filename: str) -> tuple[str, ...]: def find_executable( - exe: str, _environ: Mapping[str, str] | None = None, + exe: str, *, env: Mapping[str, str] | None = None, ) -> str | None: exe = os.path.normpath(exe) if os.sep in exe: return exe - environ = _environ if _environ is not None else os.environ + environ = env if env is not None else os.environ if 'PATHEXT' in environ: exts = environ['PATHEXT'].split(os.pathsep) @@ -43,12 +43,12 @@ def find_executable( return None -def normexe(orig: str) -> str: +def normexe(orig: str, *, env: Mapping[str, str] | None = None) -> str: def _error(msg: str) -> NoReturn: raise ExecutableNotFoundError(f'Executable `{orig}` {msg}') if os.sep not in orig and (not os.altsep or os.altsep not in orig): - exe = find_executable(orig) + exe = find_executable(orig, env=env) if exe is None: _error('not found') return exe @@ -62,7 +62,11 @@ def _error(msg: str) -> NoReturn: return orig -def normalize_cmd(cmd: tuple[str, ...]) -> tuple[str, ...]: +def normalize_cmd( + cmd: tuple[str, ...], + *, + env: Mapping[str, str] | None = None, +) -> tuple[str, ...]: """Fixes for the following issues on windows - https://bugs.python.org/issue8557 - windows does not parse shebangs @@ -70,12 +74,12 @@ def normalize_cmd(cmd: tuple[str, ...]) -> tuple[str, ...]: This function also makes deep-path shebangs work just fine """ # Use PATH to determine the executable - exe = normexe(cmd[0]) + exe = normexe(cmd[0], env=env) # Figure out the shebang from the resulting command cmd = parse_filename(exe) + (exe,) + cmd[1:] # This could have given us back another bare executable - exe = normexe(cmd[0]) + exe = normexe(cmd[0], env=env) return (exe,) + cmd[1:] diff --git a/pre_commit/util.py b/pre_commit/util.py index d51fd32d9..8ea48446a 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -99,7 +99,7 @@ def cmd_output_b( _setdefault_kwargs(kwargs) try: - cmd = parse_shebang.normalize_cmd(cmd) + cmd = parse_shebang.normalize_cmd(cmd, env=kwargs.get('env')) except parse_shebang.ExecutableNotFoundError as e: returncode, stdout_b, stderr_b = e.to_output() else: diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index e3943773f..a1ecda867 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -248,7 +248,7 @@ def test_install_idempotent(tempdir_factory, store): def _path_without_us(): # Choose a path which *probably* doesn't include us env = dict(os.environ) - exe = find_executable('pre-commit', _environ=env) + exe = find_executable('pre-commit', env=env) while exe: parts = env['PATH'].split(os.pathsep) after = [ @@ -258,7 +258,7 @@ def _path_without_us(): if parts == after: raise AssertionError(exe, parts) env['PATH'] = os.pathsep.join(after) - exe = find_executable('pre-commit', _environ=env) + exe = find_executable('pre-commit', env=env) return env['PATH'] @@ -276,18 +276,19 @@ def test_environment_not_sourced(tempdir_factory, store): # Use a specific homedir to ignore --user installs homedir = tempdir_factory.get() - ret, out = git_commit( - env={ - 'HOME': homedir, - 'PATH': _path_without_us(), - # Git needs this to make a commit - 'GIT_AUTHOR_NAME': os.environ['GIT_AUTHOR_NAME'], - 'GIT_COMMITTER_NAME': os.environ['GIT_COMMITTER_NAME'], - 'GIT_AUTHOR_EMAIL': os.environ['GIT_AUTHOR_EMAIL'], - 'GIT_COMMITTER_EMAIL': os.environ['GIT_COMMITTER_EMAIL'], - }, - check=False, - ) + env = { + 'HOME': homedir, + 'PATH': _path_without_us(), + # Git needs this to make a commit + 'GIT_AUTHOR_NAME': os.environ['GIT_AUTHOR_NAME'], + 'GIT_COMMITTER_NAME': os.environ['GIT_COMMITTER_NAME'], + 'GIT_AUTHOR_EMAIL': os.environ['GIT_AUTHOR_EMAIL'], + 'GIT_COMMITTER_EMAIL': os.environ['GIT_COMMITTER_EMAIL'], + } + if os.name == 'nt' and 'PATHEXT' in os.environ: # pragma: no cover + env['PATHEXT'] = os.environ['PATHEXT'] + + ret, out = git_commit(env=env, check=False) assert ret == 1 assert out == ( '`pre-commit` not found. ' diff --git a/tests/languages/rust_test.py b/tests/languages/rust_test.py index f011e7199..b8167a9e3 100644 --- a/tests/languages/rust_test.py +++ b/tests/languages/rust_test.py @@ -1,5 +1,6 @@ from __future__ import annotations +from typing import Mapping from unittest import mock import pytest @@ -48,7 +49,9 @@ def test_installs_with_bootstrapped_rustup(tmpdir, language_version): original_find_executable = parse_shebang.find_executable - def mocked_find_executable(exe: str) -> str | None: + def mocked_find_executable( + exe: str, *, env: Mapping[str, str] | None = None, + ) -> str | None: """ Return `None` the first time `find_executable` is called to ensure that the bootstrapping code is executed, then just let the function @@ -59,7 +62,7 @@ def mocked_find_executable(exe: str) -> str | None: find_executable_exes.append(exe) if len(find_executable_exes) == 1: return None - return original_find_executable(exe) + return original_find_executable(exe, env=env) with mock.patch.object(parse_shebang, 'find_executable') as find_exe_mck: find_exe_mck.side_effect = mocked_find_executable diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index d7acbf577..2fcb29ee7 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -75,10 +75,10 @@ def test_find_executable_path_ext(in_tmpdir): env_path = {'PATH': os.path.dirname(exe_path)} env_path_ext = dict(env_path, PATHEXT=os.pathsep.join(('.exe', '.myext'))) assert parse_shebang.find_executable('run') is None - assert parse_shebang.find_executable('run', _environ=env_path) is None - ret = parse_shebang.find_executable('run.myext', _environ=env_path) + assert parse_shebang.find_executable('run', env=env_path) is None + ret = parse_shebang.find_executable('run.myext', env=env_path) assert ret == exe_path - ret = parse_shebang.find_executable('run', _environ=env_path_ext) + ret = parse_shebang.find_executable('run', env=env_path_ext) assert ret == exe_path From 619f2bf5a9a4b03766c3304ad4e01ec90bea17f1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 9 Jan 2023 12:31:05 -0500 Subject: [PATCH 720/967] eagerly catch invalid yaml in migrate-config --- pre_commit/commands/migrate_config.py | 9 +++++++++ tests/commands/migrate_config_test.py | 13 +++++++++++++ 2 files changed, 22 insertions(+) diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index 836936b25..6f7af4eba 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -3,8 +3,10 @@ import re import textwrap +import cfgv import yaml +from pre_commit.clientlib import InvalidConfigError from pre_commit.yaml import yaml_load @@ -44,6 +46,13 @@ def migrate_config(config_file: str, quiet: bool = False) -> int: with open(config_file) as f: orig_contents = contents = f.read() + with cfgv.reraise_as(InvalidConfigError): + with cfgv.validate_context(f'File {config_file}'): + try: + yaml_load(orig_contents) + except Exception as e: + raise cfgv.ValidationError(str(e)) + contents = _migrate_map(contents) contents = _migrate_sha_to_rev(contents) diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index b80244e12..fca1ad92f 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -1,6 +1,9 @@ from __future__ import annotations +import pytest + import pre_commit.constants as C +from pre_commit.clientlib import InvalidConfigError from pre_commit.commands.migrate_config import migrate_config @@ -129,3 +132,13 @@ def test_migrate_config_sha_to_rev(tmpdir): ' rev: v1.2.0\n' ' hooks: []\n' ) + + +def test_migrate_config_invalid_yaml(tmpdir): + contents = '[' + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write(contents) + with tmpdir.as_cwd(), pytest.raises(InvalidConfigError) as excinfo: + migrate_config(C.CONFIG_FILE) + expected = '\n==> File .pre-commit-config.yaml\n=====> ' + assert str(excinfo.value).startswith(expected) From 9afd63948e2ba76cd0e351d022efd82534383146 Mon Sep 17 00:00:00 2001 From: taoufik07 Date: Tue, 3 Jan 2023 01:48:43 +0100 Subject: [PATCH 721/967] Make Go a first class language --- pre_commit/languages/golang.py | 116 ++++++++++++++++-- .../golang-hello-world/main.go | 8 +- tests/languages/golang_test.py | 43 +++++++ tests/repository_test.py | 27 +++- 4 files changed, 181 insertions(+), 13 deletions(-) create mode 100644 tests/languages/golang_test.py diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index a57c38dcd..756aa1640 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -1,9 +1,21 @@ from __future__ import annotations import contextlib +import functools +import json import os.path +import platform +import shutil import sys +import tarfile +import tempfile +import urllib.error +import urllib.request +import zipfile +from typing import ContextManager from typing import Generator +from typing import IO +from typing import Protocol from typing import Sequence import pre_commit.constants as C @@ -17,20 +29,100 @@ from pre_commit.util import rmtree ENVIRONMENT_DIR = 'golangenv' -get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check +_ARCH_ALIASES = { + 'x86_64': 'amd64', + 'i386': '386', + 'aarch64': 'arm64', + 'armv8': 'arm64', + 'armv7l': 'armv6l', +} +_ARCH = platform.machine().lower() +_ARCH = _ARCH_ALIASES.get(_ARCH, _ARCH) + + +class ExtractAll(Protocol): + def extractall(self, path: str) -> None: ... + + +if sys.platform == 'win32': # pragma: win32 cover + _EXT = 'zip' + + def _open_archive(bio: IO[bytes]) -> ContextManager[ExtractAll]: + return zipfile.ZipFile(bio) +else: # pragma: win32 no cover + _EXT = 'tar.gz' + + def _open_archive(bio: IO[bytes]) -> ContextManager[ExtractAll]: + return tarfile.open(fileobj=bio) + + +@functools.lru_cache(maxsize=1) +def get_default_version() -> str: + if helpers.exe_exists('go'): + return 'system' + else: + return C.DEFAULT + + +def get_env_patch(venv: str, version: str) -> PatchesT: + if version == 'system': + return ( + ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), + ) -def get_env_patch(venv: str) -> PatchesT: return ( - ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), + ('GOROOT', os.path.join(venv, '.go')), + ( + 'PATH', ( + os.path.join(venv, 'bin'), os.pathsep, + os.path.join(venv, '.go', 'bin'), os.pathsep, Var('PATH'), + ), + ), ) +@functools.lru_cache +def _infer_go_version(version: str) -> str: + if version != C.DEFAULT: + return version + resp = urllib.request.urlopen('https://go.dev/dl/?mode=json') + # TODO: 3.9+ .removeprefix('go') + return json.load(resp)[0]['version'][2:] + + +def _get_url(version: str) -> str: + os_name = platform.system().lower() + version = _infer_go_version(version) + return f'https://dl.google.com/go/go{version}.{os_name}-{_ARCH}.{_EXT}' + + +def _install_go(version: str, dest: str) -> None: + try: + resp = urllib.request.urlopen(_get_url(version)) + except urllib.error.HTTPError as e: # pragma: no cover + if e.code == 404: + raise ValueError( + f'Could not find a version matching your system requirements ' + f'(os={platform.system().lower()}; arch={_ARCH})', + ) from e + else: + raise + else: + with tempfile.TemporaryFile() as f: + shutil.copyfileobj(resp, f) + f.seek(0) + + with _open_archive(f) as archive: + archive.extractall(dest) + shutil.move(os.path.join(dest, 'go'), os.path.join(dest, '.go')) + + @contextlib.contextmanager -def in_env(prefix: Prefix) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) - with envcontext(get_env_patch(envdir)): +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + with envcontext(get_env_patch(envdir, version)): yield @@ -39,15 +131,23 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - helpers.assert_version_default('golang', version) env_dir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + if version != 'system': + _install_go(version, env_dir) + if sys.platform == 'cygwin': # pragma: no cover gopath = cmd_output('cygpath', '-w', env_dir)[1].strip() else: gopath = env_dir + env = dict(os.environ, GOPATH=gopath) env.pop('GOBIN', None) + if version != 'system': + env['GOROOT'] = os.path.join(env_dir, '.go') + env['PATH'] = os.pathsep.join(( + os.path.join(env_dir, '.go', 'bin'), os.environ['PATH'], + )) helpers.run_setup_cmd(prefix, ('go', 'install', './...'), env=env) for dependency in additional_dependencies: @@ -64,5 +164,5 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: - with in_env(hook.prefix): + with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/testing/resources/golang_hooks_repo/golang-hello-world/main.go b/testing/resources/golang_hooks_repo/golang-hello-world/main.go index 1e3c591a2..168574384 100644 --- a/testing/resources/golang_hooks_repo/golang-hello-world/main.go +++ b/testing/resources/golang_hooks_repo/golang-hello-world/main.go @@ -3,7 +3,9 @@ package main import ( "fmt" + "runtime" "github.com/BurntSushi/toml" + "os" ) type Config struct { @@ -11,7 +13,11 @@ type Config struct { } func main() { + message := runtime.Version() + if len(os.Args) > 1 { + message = os.Args[1] + } var conf Config toml.Decode("What = 'world'\n", &conf) - fmt.Printf("hello %v\n", conf.What) + fmt.Printf("hello %v from %s\n", conf.What, message) } diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py new file mode 100644 index 000000000..0219261fb --- /dev/null +++ b/tests/languages/golang_test.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import re +from unittest import mock + +import pytest + +import pre_commit.constants as C +from pre_commit.languages import golang +from pre_commit.languages import helpers + + +ACTUAL_GET_DEFAULT_VERSION = golang.get_default_version.__wrapped__ + + +@pytest.fixture +def exe_exists_mck(): + with mock.patch.object(helpers, 'exe_exists') as mck: + yield mck + + +def test_golang_default_version_system_available(exe_exists_mck): + exe_exists_mck.return_value = True + assert ACTUAL_GET_DEFAULT_VERSION() == 'system' + + +def test_golang_default_version_system_not_available(exe_exists_mck): + exe_exists_mck.return_value = False + assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT + + +ACTUAL_INFER_GO_VERSION = golang._infer_go_version.__wrapped__ + + +def test_golang_infer_go_version_not_default(): + assert ACTUAL_INFER_GO_VERSION('1.19.4') == '1.19.4' + + +def test_golang_infer_go_version_default(): + version = ACTUAL_INFER_GO_VERSION(C.DEFAULT) + + assert version != C.DEFAULT + assert re.match(r'^\d+\.\d+\.\d+$', version) diff --git a/tests/repository_test.py b/tests/repository_test.py index fa8bf4319..2fa1cccea 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -380,17 +380,36 @@ def test_swift_hook(tempdir_factory, store): ) -def test_golang_hook(tempdir_factory, store): +def test_golang_system_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'golang_hooks_repo', - 'golang-hook', [], b'hello world\n', + 'golang-hook', ['system'], b'hello world from system\n', + config_kwargs={ + 'hooks': [{ + 'id': 'golang-hook', + 'language_version': 'system', + }], + }, + ) + + +def test_golang_versioned_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'golang_hooks_repo', + 'golang-hook', [], b'hello world from go1.18.4\n', + config_kwargs={ + 'hooks': [{ + 'id': 'golang-hook', + 'language_version': '1.18.4', + }], + }, ) def test_golang_hook_still_works_when_gobin_is_set(tempdir_factory, store): gobin_dir = tempdir_factory.get() with envcontext((('GOBIN', gobin_dir),)): - test_golang_hook(tempdir_factory, store) + test_golang_system_hook(tempdir_factory, store) assert os.listdir(gobin_dir) == [] @@ -677,7 +696,7 @@ def test_additional_golang_dependencies_installed( envdir = helpers.environment_dir( hook.prefix, golang.ENVIRONMENT_DIR, - C.DEFAULT, + golang.get_default_version(), ) binaries = os.listdir(os.path.join(envdir, 'bin')) # normalize for windows From 37685a7f4200c50cf707ebf9cddd5700ab66f31a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 15 Jan 2023 09:56:30 -0500 Subject: [PATCH 722/967] the local repo no longer needs to be a git repo --- pre_commit/store.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index effebfb88..e42cc4897 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -204,16 +204,6 @@ def make_local_strategy(directory: str) -> None: with open(target_file, 'w') as f: f.write(contents) - env = git.no_git_env() - - # initialize the git repository so it looks more like cloned repos - def _git_cmd(*args: str) -> None: - cmd_output_b('git', *args, cwd=directory, env=env) - - git.init_repo(directory, '<>') - _git_cmd('add', '.') - git.commit(repo=directory) - return self._new_repo( 'local', C.LOCAL_REPO_VERSION, deps, make_local_strategy, ) From ae34a962d79d4e823214028c353f690dd2ad4306 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 2 Jan 2023 19:02:38 -0500 Subject: [PATCH 723/967] make in_env part of the language api --- pre_commit/commands/run.py | 3 ++- pre_commit/languages/all.py | 9 +++++++++ pre_commit/languages/conda.py | 10 +++------- pre_commit/languages/coursier.py | 12 ++++-------- pre_commit/languages/dart.py | 8 +++----- pre_commit/languages/docker.py | 4 ++-- pre_commit/languages/docker_image.py | 1 + pre_commit/languages/dotnet.py | 8 +++----- pre_commit/languages/fail.py | 1 + pre_commit/languages/golang.py | 3 +-- pre_commit/languages/helpers.py | 9 ++++++++- pre_commit/languages/lua.py | 12 +++++------- pre_commit/languages/node.py | 10 +++------- pre_commit/languages/perl.py | 10 +++------- pre_commit/languages/pygrep.py | 1 + pre_commit/languages/python.py | 10 +++------- pre_commit/languages/r.py | 14 +++++--------- pre_commit/languages/ruby.py | 12 ++++-------- pre_commit/languages/rust.py | 12 ++++-------- pre_commit/languages/script.py | 1 + pre_commit/languages/swift.py | 10 ++++------ pre_commit/languages/system.py | 2 +- tests/repository_test.py | 3 ++- 23 files changed, 73 insertions(+), 92 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 429e04c60..a398e84c5 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -189,7 +189,8 @@ def _run_single_hook( filenames = () time_before = time.time() language = languages[hook.language] - retcode, out = language.run_hook(hook, filenames, use_color) + with language.in_env(hook.prefix, hook.language_version): + retcode, out = language.run_hook(hook, filenames, use_color) duration = round(time.time() - time_before, 2) or 0 diff_after = _get_diff() diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 7c7c58bde..6135272ac 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -1,5 +1,6 @@ from __future__ import annotations +from typing import ContextManager from typing import Protocol from typing import Sequence @@ -50,6 +51,14 @@ def install_environment( ) -> None: ... + # modify the environment for hook execution + def in_env( + self, + prefix: Prefix, + version: str, + ) -> ContextManager[None]: + ... + # execute a hook and return the exit code and output def run_hook( self, diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index 5a0a720f3..612a8242d 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -40,11 +40,8 @@ def get_env_patch(env: str) -> PatchesT: @contextlib.contextmanager -def in_env( - prefix: Prefix, - language_version: str, -) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -88,5 +85,4 @@ def run_hook( # can run them without which is much quicker and produces a better # output. # cmd = ('conda', 'run', '-p', env_dir) + hook.cmd - with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index fdea3cd71..46eb4e0a2 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -59,12 +59,9 @@ def get_env_patch(target_dir: str) -> PatchesT: # pragma: win32 no cover ) -@contextlib.contextmanager -def in_env( - prefix: Prefix, - language_version: str, -) -> Generator[None, None, None]: # pragma: win32 no cover - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) +@contextlib.contextmanager # pragma: win32 no cover +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -74,5 +71,4 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: # pragma: win32 no cover - with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py index 223567a54..7d1322b00 100644 --- a/pre_commit/languages/dart.py +++ b/pre_commit/languages/dart.py @@ -7,7 +7,6 @@ from typing import Generator from typing import Sequence -import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var @@ -30,8 +29,8 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager -def in_env(prefix: Prefix) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -103,5 +102,4 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: - with in_env(hook.prefix): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index c51cf7c10..dbdfd35c5 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -5,7 +5,6 @@ import os from typing import Sequence -import pre_commit.constants as C from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix @@ -16,6 +15,7 @@ PRE_COMMIT_LABEL = 'PRE_COMMIT' get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check +in_env = helpers.no_env # no special environment for docker def _is_in_docker() -> bool: @@ -94,7 +94,7 @@ def install_environment( helpers.assert_version_default('docker', version) helpers.assert_no_additional_deps('docker', additional_dependencies) - directory = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) + directory = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) # Docker doesn't really have relevant disk environment, but pre-commit # still needs to cleanup its state files on failure diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index daa4d1ba3..b1cd3caf8 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -10,6 +10,7 @@ get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check install_environment = helpers.no_install +in_env = helpers.no_env def run_hook( diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index 0bb0210cd..8d4d48e3d 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -9,7 +9,6 @@ from typing import Generator from typing import Sequence -import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var @@ -31,8 +30,8 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager -def in_env(prefix: Prefix) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -121,5 +120,4 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: - with in_env(hook.prefix): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index 00b06a9a9..f051d5e40 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -9,6 +9,7 @@ get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check install_environment = helpers.no_install +in_env = helpers.no_env def run_hook( diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 756aa1640..b38e4994c 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -164,5 +164,4 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: - with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 098e95c5a..5b3a54ffd 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -1,10 +1,12 @@ from __future__ import annotations +import contextlib import multiprocessing import os import random import re from typing import Any +from typing import Generator from typing import NoReturn from typing import Sequence @@ -84,7 +86,12 @@ def no_install( version: str, additional_dependencies: Sequence[str], ) -> NoReturn: - raise AssertionError('This type is not installable') + raise AssertionError('This language is not installable') + + +@contextlib.contextmanager +def no_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + yield def target_concurrency(hook: Hook) -> int: diff --git a/pre_commit/languages/lua.py b/pre_commit/languages/lua.py index 26c8f1b7d..1c872f361 100644 --- a/pre_commit/languages/lua.py +++ b/pre_commit/languages/lua.py @@ -6,7 +6,6 @@ from typing import Generator from typing import Sequence -import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var @@ -45,8 +44,8 @@ def get_env_patch(d: str) -> PatchesT: # pragma: win32 no cover @contextlib.contextmanager # pragma: win32 no cover -def in_env(prefix: Prefix) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -58,8 +57,8 @@ def install_environment( ) -> None: # pragma: win32 no cover helpers.assert_version_default('lua', version) - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) - with in_env(prefix): + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + with in_env(prefix, version): # luarocks doesn't bootstrap a tree prior to installing # so ensure the directory exists. os.makedirs(envdir, exist_ok=True) @@ -81,5 +80,4 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: # pragma: win32 no cover - with in_env(hook.prefix): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 8facfe007..7b4d2e7d4 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -59,11 +59,8 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager -def in_env( - prefix: Prefix, - language_version: str, -) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -118,5 +115,4 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: - with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py index 95be65599..622c8a129 100644 --- a/pre_commit/languages/perl.py +++ b/pre_commit/languages/perl.py @@ -33,11 +33,8 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager -def in_env( - prefix: Prefix, - language_version: str, -) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -58,5 +55,4 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: - with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 2e2072b08..d9f779f77 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -16,6 +16,7 @@ get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check install_environment = helpers.no_install +in_env = helpers.no_env def _process_filename_by_line(pattern: Pattern[bytes], filename: str) -> int: diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index a7744d642..28f4ab5d5 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -152,11 +152,8 @@ def norm_version(version: str) -> str | None: @contextlib.contextmanager -def in_env( - prefix: Prefix, - language_version: str, -) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -222,5 +219,4 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: - with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index d2ec83daa..f5e1eaba2 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -30,11 +30,8 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager -def in_env( - prefix: Prefix, - language_version: str, -) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -156,7 +153,6 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: - with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs( - hook, _cmd_from_hook(hook), file_args, color=color, - ) + return helpers.run_xargs( + hook, _cmd_from_hook(hook), file_args, color=color, + ) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 89af25459..2805aca63 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -67,12 +67,9 @@ def get_env_patch( @contextlib.contextmanager -def in_env( - prefix: Prefix, - language_version: str, -) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) - with envcontext(get_env_patch(envdir, language_version)): +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + with envcontext(get_env_patch(envdir, version)): yield @@ -143,5 +140,4 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: - with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 0f6cd332d..9da8f82ce 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -62,12 +62,9 @@ def get_env_patch(target_dir: str, version: str) -> PatchesT: @contextlib.contextmanager -def in_env( - prefix: Prefix, - language_version: str, -) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) - with envcontext(get_env_patch(envdir, language_version)): +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + with envcontext(get_env_patch(envdir, version)): yield @@ -164,5 +161,4 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: - with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index d5e7677f9..5b7bdd5f1 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -9,6 +9,7 @@ get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check install_environment = helpers.no_install +in_env = helpers.no_env def run_hook( diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 7cc61d958..ad00b94ac 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -5,7 +5,6 @@ from typing import Generator from typing import Sequence -import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var @@ -27,8 +26,8 @@ def get_env_patch(venv: str) -> PatchesT: # pragma: win32 no cover @contextlib.contextmanager # pragma: win32 no cover -def in_env(prefix: Prefix) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -38,7 +37,7 @@ def install_environment( ) -> None: # pragma: win32 no cover helpers.assert_version_default('swift', version) helpers.assert_no_additional_deps('swift', additional_dependencies) - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) # Build the swift package os.mkdir(envdir) @@ -55,5 +54,4 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: # pragma: win32 no cover - with in_env(hook.prefix): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index c64fb3650..9cc94f8c9 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -5,11 +5,11 @@ from pre_commit.hook import Hook from pre_commit.languages import helpers - ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check install_environment = helpers.no_install +in_env = helpers.no_env def run_hook( diff --git a/tests/repository_test.py b/tests/repository_test.py index 2fa1cccea..236c7983b 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -44,7 +44,8 @@ def _norm_out(b): def _hook_run(hook, filenames, color): - return languages[hook.language].run_hook(hook, filenames, color) + with languages[hook.language].in_env(hook.prefix, hook.language_version): + return languages[hook.language].run_hook(hook, filenames, color) def _get_hook_no_install(repo_config, store, hook_id): From 628c876b2d0e1fce25c38e6455a61351d57c714f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 16 Jan 2023 16:34:01 -0500 Subject: [PATCH 724/967] adjust the run_hook api to no longer take Hook --- pre_commit/commands/run.py | 9 +++++- pre_commit/hook.py | 5 --- pre_commit/languages/all.py | 7 +++-- pre_commit/languages/conda.py | 14 +-------- pre_commit/languages/coursier.py | 10 +----- pre_commit/languages/dart.py | 10 +----- pre_commit/languages/docker.py | 20 ++++++++---- pre_commit/languages/docker_image.py | 17 +++++++--- pre_commit/languages/dotnet.py | 10 +----- pre_commit/languages/fail.py | 10 ++++-- pre_commit/languages/golang.py | 10 +----- pre_commit/languages/helpers.py | 46 ++++++++++++++++++++++------ pre_commit/languages/lua.py | 10 +----- pre_commit/languages/node.py | 10 +----- pre_commit/languages/perl.py | 10 +----- pre_commit/languages/pygrep.py | 12 +++++--- pre_commit/languages/python.py | 10 +----- pre_commit/languages/r.py | 29 +++++++++++------- pre_commit/languages/ruby.py | 10 +----- pre_commit/languages/rust.py | 10 +----- pre_commit/languages/script.py | 18 ++++++++--- pre_commit/languages/swift.py | 15 +++------ pre_commit/languages/system.py | 12 +------- tests/languages/helpers_test.py | 28 ++++++++--------- tests/languages/r_test.py | 4 +-- tests/repository_test.py | 9 +++++- 26 files changed, 163 insertions(+), 192 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index a398e84c5..85fa59aa1 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -190,7 +190,14 @@ def _run_single_hook( time_before = time.time() language = languages[hook.language] with language.in_env(hook.prefix, hook.language_version): - retcode, out = language.run_hook(hook, filenames, use_color) + retcode, out = language.run_hook( + hook.prefix, + hook.entry, + hook.args, + filenames, + require_serial=hook.require_serial, + color=use_color, + ) duration = round(time.time() - time_before, 2) or 0 diff_after = _get_diff() diff --git a/pre_commit/hook.py b/pre_commit/hook.py index 202abb358..6d436ca30 100644 --- a/pre_commit/hook.py +++ b/pre_commit/hook.py @@ -1,7 +1,6 @@ from __future__ import annotations import logging -import shlex from typing import Any from typing import NamedTuple from typing import Sequence @@ -37,10 +36,6 @@ class Hook(NamedTuple): stages: Sequence[str] verbose: bool - @property - def cmd(self) -> tuple[str, ...]: - return (*shlex.split(self.entry), *self.args) - @property def install_key(self) -> tuple[Prefix, str, str, tuple[str, ...]]: return ( diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 6135272ac..c7aab65e7 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -4,7 +4,6 @@ from typing import Protocol from typing import Sequence -from pre_commit.hook import Hook from pre_commit.languages import conda from pre_commit.languages import coursier from pre_commit.languages import dart @@ -62,8 +61,12 @@ def in_env( # execute a hook and return the exit code and output def run_hook( self, - hook: Hook, + prefix: Prefix, + entry: str, + args: Sequence[str], file_args: Sequence[str], + *, + require_serial: bool, color: bool, ) -> tuple[int, bytes]: ... diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index 612a8242d..e2fb01969 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -10,7 +10,6 @@ from pre_commit.envcontext import SubstitutionT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b @@ -18,6 +17,7 @@ ENVIRONMENT_DIR = 'conda' get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check +run_hook = helpers.basic_run_hook def get_env_patch(env: str) -> PatchesT: @@ -74,15 +74,3 @@ def install_environment( conda_exe, 'install', '-p', env_dir, *additional_dependencies, cwd=prefix.prefix_dir, ) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - # TODO: Some rare commands need to be run using `conda run` but mostly we - # can run them without which is much quicker and produces a better - # output. - # cmd = ('conda', 'run', '-p', env_dir) + hook.cmd - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index 46eb4e0a2..a6aea3fb2 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -8,7 +8,6 @@ from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.parse_shebang import find_executable from pre_commit.prefix import Prefix @@ -17,6 +16,7 @@ get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check +run_hook = helpers.basic_run_hook def install_environment( @@ -64,11 +64,3 @@ def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: # pragma: win32 no cover - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py index 7d1322b00..e3c1c5855 100644 --- a/pre_commit/languages/dart.py +++ b/pre_commit/languages/dart.py @@ -10,7 +10,6 @@ from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import win_exe @@ -20,6 +19,7 @@ get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check +run_hook = helpers.basic_run_hook def get_env_patch(venv: str) -> PatchesT: @@ -95,11 +95,3 @@ def _install_dir(prefix_p: Prefix, pub_cache: str) -> None: raise AssertionError( f'could not find pubspec.yaml for {dep_s}', ) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index dbdfd35c5..18234567b 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -5,7 +5,6 @@ import os from typing import Sequence -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError @@ -123,16 +122,25 @@ def docker_cmd() -> tuple[str, ...]: # pragma: win32 no cover def run_hook( - hook: Hook, + prefix: Prefix, + entry: str, + args: Sequence[str], file_args: Sequence[str], + *, + require_serial: bool, color: bool, ) -> tuple[int, bytes]: # pragma: win32 no cover # Rebuild the docker image in case it has gone missing, as many people do # automated cleanup of docker images. - build_docker_image(hook.prefix, pull=False) + build_docker_image(prefix, pull=False) - entry_exe, *cmd_rest = hook.cmd + entry_exe, *cmd_rest = helpers.hook_cmd(entry, args) - entry_tag = ('--entrypoint', entry_exe, docker_tag(hook.prefix)) + entry_tag = ('--entrypoint', entry_exe, docker_tag(prefix)) cmd = (*docker_cmd(), *entry_tag, *cmd_rest) - return helpers.run_xargs(hook, cmd, file_args, color=color) + return helpers.run_xargs( + cmd, + file_args, + require_serial=require_serial, + color=color, + ) diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index b1cd3caf8..230983823 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -2,9 +2,9 @@ from typing import Sequence -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.languages.docker import docker_cmd +from pre_commit.prefix import Prefix ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version @@ -14,9 +14,18 @@ def run_hook( - hook: Hook, + prefix: Prefix, + entry: str, + args: Sequence[str], file_args: Sequence[str], + *, + require_serial: bool, color: bool, ) -> tuple[int, bytes]: # pragma: win32 no cover - cmd = docker_cmd() + hook.cmd - return helpers.run_xargs(hook, cmd, file_args, color=color) + cmd = docker_cmd() + helpers.hook_cmd(entry, args) + return helpers.run_xargs( + cmd, + file_args, + require_serial=require_serial, + color=color, + ) diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index 8d4d48e3d..4c3955e85 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -12,7 +12,6 @@ from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix @@ -21,6 +20,7 @@ get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check +run_hook = helpers.basic_run_hook def get_env_patch(venv: str) -> PatchesT: @@ -113,11 +113,3 @@ def install_environment( # Clean the git dir, ignoring the environment dir clean_cmd = ('git', 'clean', '-ffxd', '-e', f'{ENVIRONMENT_DIR}-*') helpers.run_setup_cmd(prefix, clean_cmd) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index f051d5e40..13b2bc12c 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -2,8 +2,8 @@ from typing import Sequence -from pre_commit.hook import Hook from pre_commit.languages import helpers +from pre_commit.prefix import Prefix ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version @@ -13,10 +13,14 @@ def run_hook( - hook: Hook, + prefix: Prefix, + entry: str, + args: Sequence[str], file_args: Sequence[str], + *, + require_serial: bool, color: bool, ) -> tuple[int, bytes]: - out = f'{hook.entry}\n\n'.encode() + out = f'{entry}\n\n'.encode() out += b'\n'.join(f.encode() for f in file_args) + b'\n' return 1, out diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index b38e4994c..3c4b652fa 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -22,7 +22,6 @@ from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output @@ -30,6 +29,7 @@ ENVIRONMENT_DIR = 'golangenv' health_check = helpers.basic_health_check +run_hook = helpers.basic_run_hook _ARCH_ALIASES = { 'x86_64': 'amd64', @@ -157,11 +157,3 @@ def install_environment( pkgdir = os.path.join(env_dir, 'pkg') if os.path.exists(pkgdir): # pragma: no branch (always true on windows?) rmtree(pkgdir) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 5b3a54ffd..074f98e9f 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -5,6 +5,7 @@ import os import random import re +import shlex from typing import Any from typing import Generator from typing import NoReturn @@ -12,7 +13,6 @@ import pre_commit.constants as C from pre_commit import parse_shebang -from pre_commit.hook import Hook from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b from pre_commit.xargs import xargs @@ -94,8 +94,8 @@ def no_env(prefix: Prefix, version: str) -> Generator[None, None, None]: yield -def target_concurrency(hook: Hook) -> int: - if hook.require_serial or 'PRE_COMMIT_NO_CONCURRENCY' in os.environ: +def target_concurrency() -> int: + if 'PRE_COMMIT_NO_CONCURRENCY' in os.environ: return 1 else: # Travis appears to have a bunch of CPUs, but we can't use them all. @@ -119,13 +119,39 @@ def _shuffled(seq: Sequence[str]) -> list[str]: def run_xargs( - hook: Hook, cmd: tuple[str, ...], file_args: Sequence[str], - **kwargs: Any, + *, + require_serial: bool, + color: bool, ) -> tuple[int, bytes]: - # Shuffle the files so that they more evenly fill out the xargs partitions, - # but do it deterministically in case a hook cares about ordering. - file_args = _shuffled(file_args) - kwargs['target_concurrency'] = target_concurrency(hook) - return xargs(cmd, file_args, **kwargs) + if require_serial: + jobs = 1 + else: + # Shuffle the files so that they more evenly fill out the xargs + # partitions, but do it deterministically in case a hook cares about + # ordering. + file_args = _shuffled(file_args) + jobs = target_concurrency() + return xargs(cmd, file_args, target_concurrency=jobs, color=color) + + +def hook_cmd(entry: str, args: Sequence[str]) -> tuple[str, ...]: + return (*shlex.split(entry), *args) + + +def basic_run_hook( + prefix: Prefix, + entry: str, + args: Sequence[str], + file_args: Sequence[str], + *, + require_serial: bool, + color: bool, +) -> tuple[int, bytes]: + return run_xargs( + hook_cmd(entry, args), + file_args, + require_serial=require_serial, + color=color, + ) diff --git a/pre_commit/languages/lua.py b/pre_commit/languages/lua.py index 1c872f361..ffc40b505 100644 --- a/pre_commit/languages/lua.py +++ b/pre_commit/languages/lua.py @@ -9,7 +9,6 @@ from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output @@ -17,6 +16,7 @@ ENVIRONMENT_DIR = 'lua_env' get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check +run_hook = helpers.basic_run_hook def _get_lua_version() -> str: # pragma: win32 no cover @@ -73,11 +73,3 @@ def install_environment( for dependency in additional_dependencies: cmd = ('luarocks', '--tree', envdir, 'install', dependency) helpers.run_setup_cmd(prefix, cmd) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: # pragma: win32 no cover - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 7b4d2e7d4..9688da359 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -12,7 +12,6 @@ from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.languages.python import bin_dir from pre_commit.prefix import Prefix @@ -21,6 +20,7 @@ from pre_commit.util import rmtree ENVIRONMENT_DIR = 'node_env' +run_hook = helpers.basic_run_hook @functools.lru_cache(maxsize=1) @@ -108,11 +108,3 @@ def install_environment( if prefix.exists('node_modules'): # pragma: win32 no cover rmtree(prefix.path('node_modules')) os.remove(pkg) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py index 622c8a129..2530c0ee1 100644 --- a/pre_commit/languages/perl.py +++ b/pre_commit/languages/perl.py @@ -9,13 +9,13 @@ from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix ENVIRONMENT_DIR = 'perl_env' get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check +run_hook = helpers.basic_run_hook def get_env_patch(venv: str) -> PatchesT: @@ -48,11 +48,3 @@ def install_environment( helpers.run_setup_cmd( prefix, ('cpan', '-T', '.', *additional_dependencies), ) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index d9f779f77..93e2a65bd 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -8,8 +8,8 @@ from typing import Sequence from pre_commit import output -from pre_commit.hook import Hook from pre_commit.languages import helpers +from pre_commit.prefix import Prefix from pre_commit.xargs import xargs ENVIRONMENT_DIR = None @@ -88,12 +88,16 @@ class Choice(NamedTuple): def run_hook( - hook: Hook, + prefix: Prefix, + entry: str, + args: Sequence[str], file_args: Sequence[str], + *, + require_serial: bool, color: bool, ) -> tuple[int, bytes]: - exe = (sys.executable, '-m', __name__) + tuple(hook.args) + (hook.entry,) - return xargs(exe, file_args, color=color) + cmd = (sys.executable, '-m', __name__, *args, entry) + return xargs(cmd, file_args, color=color) def main(argv: Sequence[str] | None = None) -> int: diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 28f4ab5d5..c373646bc 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -12,7 +12,6 @@ from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.parse_shebang import find_executable from pre_commit.prefix import Prefix @@ -22,6 +21,7 @@ from pre_commit.util import win_exe ENVIRONMENT_DIR = 'py_env' +run_hook = helpers.basic_run_hook @functools.lru_cache(maxsize=None) @@ -212,11 +212,3 @@ def install_environment( cmd_output_b(*venv_cmd, cwd='/') with in_env(prefix, version): helpers.run_setup_cmd(prefix, install_cmd) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index f5e1eaba2..7ed3eafcd 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -10,7 +10,6 @@ from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b @@ -70,15 +69,15 @@ def _entry_validate(entry: list[str]) -> None: ) -def _cmd_from_hook(hook: Hook) -> tuple[str, ...]: - entry = shlex.split(hook.entry) - _entry_validate(entry) +def _cmd_from_hook( + prefix: Prefix, + entry: str, + args: Sequence[str], +) -> tuple[str, ...]: + cmd = shlex.split(entry) + _entry_validate(cmd) - return ( - entry[0], *RSCRIPT_OPTS, - *_prefix_if_file_entry(entry, hook.prefix), - *hook.args, - ) + return (cmd[0], *RSCRIPT_OPTS, *_prefix_if_file_entry(cmd, prefix), *args) def install_environment( @@ -149,10 +148,18 @@ def _inline_r_setup(code: str) -> str: def run_hook( - hook: Hook, + prefix: Prefix, + entry: str, + args: Sequence[str], file_args: Sequence[str], + *, + require_serial: bool, color: bool, ) -> tuple[int, bytes]: + cmd = _cmd_from_hook(prefix, entry, args) return helpers.run_xargs( - hook, _cmd_from_hook(hook), file_args, color=color, + cmd, + file_args, + require_serial=require_serial, + color=color, ) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 2805aca63..4416f7280 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -13,7 +13,6 @@ from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError @@ -21,6 +20,7 @@ ENVIRONMENT_DIR = 'rbenv' health_check = helpers.basic_health_check +run_hook = helpers.basic_run_hook @functools.lru_cache(maxsize=1) @@ -133,11 +133,3 @@ def install_environment( *prefix.star('.gem'), *additional_dependencies, ), ) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 9da8f82ce..391fd8657 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -15,7 +15,6 @@ from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b @@ -24,6 +23,7 @@ ENVIRONMENT_DIR = 'rustenv' health_check = helpers.basic_health_check +run_hook = helpers.basic_run_hook @functools.lru_cache(maxsize=1) @@ -154,11 +154,3 @@ def install_environment( 'cargo', 'install', '--bins', '--root', envdir, *args, cwd=prefix.prefix_dir, ) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 5b7bdd5f1..41fffdf07 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -2,8 +2,8 @@ from typing import Sequence -from pre_commit.hook import Hook from pre_commit.languages import helpers +from pre_commit.prefix import Prefix ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version @@ -13,9 +13,19 @@ def run_hook( - hook: Hook, + prefix: Prefix, + entry: str, + args: Sequence[str], file_args: Sequence[str], + *, + require_serial: bool, color: bool, ) -> tuple[int, bytes]: - cmd = (hook.prefix.path(hook.cmd[0]), *hook.cmd[1:]) - return helpers.run_xargs(hook, cmd, file_args, color=color) + cmd = helpers.hook_cmd(entry, args) + cmd = (prefix.path(cmd[0]), *cmd[1:]) + return helpers.run_xargs( + cmd, + file_args, + require_serial=require_serial, + color=color, + ) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index ad00b94ac..c66ad5fb0 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -8,16 +8,17 @@ from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b +BUILD_DIR = '.build' +BUILD_CONFIG = 'release' + ENVIRONMENT_DIR = 'swift_env' get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check -BUILD_DIR = '.build' -BUILD_CONFIG = 'release' +run_hook = helpers.basic_run_hook def get_env_patch(venv: str) -> PatchesT: # pragma: win32 no cover @@ -47,11 +48,3 @@ def install_environment( '-c', BUILD_CONFIG, '--build-path', os.path.join(envdir, BUILD_DIR), ) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: # pragma: win32 no cover - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 9cc94f8c9..204cad727 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -1,8 +1,5 @@ from __future__ import annotations -from typing import Sequence - -from pre_commit.hook import Hook from pre_commit.languages import helpers ENVIRONMENT_DIR = None @@ -10,11 +7,4 @@ health_check = helpers.basic_health_check install_environment = helpers.no_install in_env = helpers.no_env - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) +run_hook = helpers.basic_run_hook diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index f333e79d5..c209e7e6d 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -12,7 +12,6 @@ from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError -from testing.auto_namedtuple import auto_namedtuple @pytest.fixture @@ -94,31 +93,22 @@ def test_assert_no_additional_deps(): ) -SERIAL_FALSE = auto_namedtuple(require_serial=False) -SERIAL_TRUE = auto_namedtuple(require_serial=True) - - def test_target_concurrency_normal(): with mock.patch.object(multiprocessing, 'cpu_count', return_value=123): with mock.patch.dict(os.environ, {}, clear=True): - assert helpers.target_concurrency(SERIAL_FALSE) == 123 - - -def test_target_concurrency_cpu_count_require_serial_true(): - with mock.patch.dict(os.environ, {}, clear=True): - assert helpers.target_concurrency(SERIAL_TRUE) == 1 + assert helpers.target_concurrency() == 123 def test_target_concurrency_testing_env_var(): with mock.patch.dict( os.environ, {'PRE_COMMIT_NO_CONCURRENCY': '1'}, clear=True, ): - assert helpers.target_concurrency(SERIAL_FALSE) == 1 + assert helpers.target_concurrency() == 1 def test_target_concurrency_on_travis(): with mock.patch.dict(os.environ, {'TRAVIS': '1'}, clear=True): - assert helpers.target_concurrency(SERIAL_FALSE) == 2 + assert helpers.target_concurrency() == 2 def test_target_concurrency_cpu_count_not_implemented(): @@ -126,10 +116,20 @@ def test_target_concurrency_cpu_count_not_implemented(): multiprocessing, 'cpu_count', side_effect=NotImplementedError, ): with mock.patch.dict(os.environ, {}, clear=True): - assert helpers.target_concurrency(SERIAL_FALSE) == 1 + assert helpers.target_concurrency() == 1 def test_shuffled_is_deterministic(): seq = [str(i) for i in range(10)] expected = ['4', '0', '5', '1', '8', '6', '2', '3', '7', '9'] assert helpers._shuffled(seq) == expected + + +def test_xargs_require_serial_is_not_shuffled(): + ret, out = helpers.run_xargs( + ('echo',), [str(i) for i in range(10)], + require_serial=True, + color=False, + ) + assert ret == 0 + assert out.strip() == b'0 1 2 3 4 5 6 7 8 9' diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py index c653a3ccf..d23441401 100644 --- a/tests/languages/r_test.py +++ b/tests/languages/r_test.py @@ -23,7 +23,7 @@ def _test_r_parsing( repo = make_repo(tempdir_factory, 'r_hooks_repo') config = make_config_from_repo(repo) hook = _get_hook_no_install(config, store, hook_id) - ret = r._cmd_from_hook(hook) + ret = r._cmd_from_hook(hook.prefix, hook.entry, hook.args) expected_path = os.path.join(hook.prefix.prefix_dir, f'{hook_id}.R') expected = ( 'Rscript', @@ -111,7 +111,7 @@ def test_r_parsing_file_local(tempdir_factory, store): }], } hook = _get_hook_no_install(config, store, 'local-r') - ret = r._cmd_from_hook(hook) + ret = r._cmd_from_hook(hook.prefix, hook.entry, hook.args) assert ret == ( 'Rscript', '--no-save', '--no-restore', '--no-site-file', '--no-environ', diff --git a/tests/repository_test.py b/tests/repository_test.py index 236c7983b..4043491bc 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -45,7 +45,14 @@ def _norm_out(b): def _hook_run(hook, filenames, color): with languages[hook.language].in_env(hook.prefix, hook.language_version): - return languages[hook.language].run_hook(hook, filenames, color) + return languages[hook.language].run_hook( + hook.prefix, + hook.entry, + hook.args, + filenames, + require_serial=hook.require_serial, + color=color, + ) def _get_hook_no_install(repo_config, store, hook_id): From 70bfd76ced283f48ab8c8a5f17e9218c0b0b5d37 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 16 Jan 2023 18:33:40 -0500 Subject: [PATCH 725/967] coursier: additional_dependencies support --- .github/actions/pre-test/action.yml | 1 + pre_commit/languages/coursier.py | 47 ++++++++++++------- testing/get-coursier.ps1 | 11 ----- testing/get-coursier.sh | 34 ++++++++++---- testing/language_helpers.py | 33 +++++++++++++ .../.pre-commit-channel/echo-java.json | 8 ---- .../.pre-commit-hooks.yaml | 5 -- testing/util.py | 4 -- tests/languages/coursier_test.py | 45 ++++++++++++++++++ tests/repository_test.py | 10 ---- 10 files changed, 132 insertions(+), 66 deletions(-) delete mode 100755 testing/get-coursier.ps1 create mode 100644 testing/language_helpers.py delete mode 100644 testing/resources/coursier_hooks_repo/.pre-commit-channel/echo-java.json delete mode 100644 testing/resources/coursier_hooks_repo/.pre-commit-hooks.yaml create mode 100644 tests/languages/coursier_test.py diff --git a/.github/actions/pre-test/action.yml b/.github/actions/pre-test/action.yml index a7bf0abed..608c0cd11 100644 --- a/.github/actions/pre-test/action.yml +++ b/.github/actions/pre-test/action.yml @@ -19,6 +19,7 @@ runs: echo 'C:\Strawberry\perl\site\bin' >> "$GITHUB_PATH" echo 'C:\Strawberry\c\bin' >> "$GITHUB_PATH" + testing/get-coursier.sh testing/get-dart.sh - name: setup (linux) shell: bash diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index a6aea3fb2..69c877d32 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -1,13 +1,14 @@ from __future__ import annotations import contextlib -import os +import os.path from typing import Generator from typing import Sequence from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var +from pre_commit.errors import FatalError from pre_commit.languages import helpers from pre_commit.parse_shebang import find_executable from pre_commit.prefix import Prefix @@ -23,9 +24,8 @@ def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], -) -> None: # pragma: win32 no cover +) -> None: helpers.assert_version_default('coursier', version) - helpers.assert_no_additional_deps('coursier', additional_dependencies) # Support both possible executable names (either "cs" or "coursier") executable = find_executable('cs') or find_executable('coursier') @@ -37,29 +37,40 @@ def install_environment( envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) channel = prefix.path('.pre-commit-channel') - for app_descriptor in os.listdir(channel): - _, app_file = os.path.split(app_descriptor) - app, _ = os.path.splitext(app_file) - helpers.run_setup_cmd( - prefix, - ( - executable, - 'install', - '--default-channels=false', - f'--channel={channel}', - app, - f'--dir={envdir}', - ), + if os.path.isdir(channel): + for app_descriptor in os.listdir(channel): + _, app_file = os.path.split(app_descriptor) + app, _ = os.path.splitext(app_file) + helpers.run_setup_cmd( + prefix, + ( + executable, + 'install', + '--default-channels=false', + '--channel', channel, + '--dir', envdir, + app, + ), + ) + elif not additional_dependencies: + raise FatalError( + 'expected .pre-commit-channel dir or additional_dependencies', ) + if additional_dependencies: + install_cmd = ( + executable, 'install', '--dir', envdir, *additional_dependencies, + ) + helpers.run_setup_cmd(prefix, install_cmd) + -def get_env_patch(target_dir: str) -> PatchesT: # pragma: win32 no cover +def get_env_patch(target_dir: str) -> PatchesT: return ( ('PATH', (target_dir, os.pathsep, Var('PATH'))), ) -@contextlib.contextmanager # pragma: win32 no cover +@contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): diff --git a/testing/get-coursier.ps1 b/testing/get-coursier.ps1 deleted file mode 100755 index 42e563549..000000000 --- a/testing/get-coursier.ps1 +++ /dev/null @@ -1,11 +0,0 @@ -$wc = New-Object System.Net.WebClient - -$coursier_url = "https://github.com/coursier/coursier/releases/download/v2.0.5/cs-x86_64-pc-win32.exe" -$coursier_dest = "C:\coursier\cs.exe" -$coursier_hash ="d63d497f7805261e1cd657b8aaa626f6b8f7264cdb68219b2e6be9dd882033a9" - -New-Item -Path "C:\" -Name "coursier" -ItemType "directory" -$wc.DownloadFile($coursier_url, $coursier_dest) -if ((Get-FileHash $coursier_dest -Algorithm SHA256).Hash -ne $coursier_hash) { - throw "Invalid coursier file" -} diff --git a/testing/get-coursier.sh b/testing/get-coursier.sh index 6033c3e35..958e73b24 100755 --- a/testing/get-coursier.sh +++ b/testing/get-coursier.sh @@ -1,15 +1,29 @@ #!/usr/bin/env bash -# This is a script used in CI to install coursier set -euo pipefail -COURSIER_URL="https://github.com/coursier/coursier/releases/download/v2.0.0/cs-x86_64-pc-linux" -COURSIER_HASH="e2e838b75bc71b16bcb77ce951ad65660c89bda7957c79a0628ec7146d35122f" -ARTIFACT="/tmp/coursier/cs" +if [ "$OSTYPE" = msys ]; then + URL='https://github.com/coursier/coursier/releases/download/v2.1.0-RC4/cs-x86_64-pc-win32.zip' + SHA256='0d07386ff0f337e3e6264f7dde29d137dda6eaa2385f29741435e0b93ccdb49d' + TARGET='/tmp/coursier/cs.zip' -mkdir -p /tmp/coursier -rm -f "$ARTIFACT" -curl --location --silent --output "$ARTIFACT" "$COURSIER_URL" -echo "$COURSIER_HASH $ARTIFACT" | sha256sum --check -chmod ugo+x /tmp/coursier/cs + unpack() { + unzip "$TARGET" -d /tmp/coursier + mv /tmp/coursier/cs-*.exe /tmp/coursier/cs.exe + cygpath -w /tmp/coursier >> "$GITHUB_PATH" + } +else + URL='https://github.com/coursier/coursier/releases/download/v2.1.0-RC4/cs-x86_64-pc-linux.gz' + SHA256='176e92e08ab292531aa0c4993dbc9f2c99dec79578752f3b9285f54f306db572' + TARGET=/tmp/coursier/cs.gz + + unpack() { + gunzip "$TARGET" + chmod +x /tmp/coursier/cs + echo /tmp/coursier >> "$GITHUB_PATH" + } +fi -echo '/tmp/coursier' >> "$GITHUB_PATH" +mkdir -p /tmp/coursier +curl --location --silent --output "$TARGET" "$URL" +echo "$SHA256 $TARGET" | sha256sum --check +unpack diff --git a/testing/language_helpers.py b/testing/language_helpers.py new file mode 100644 index 000000000..02e47a002 --- /dev/null +++ b/testing/language_helpers.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import os +from typing import Sequence + +import pre_commit.constants as C +from pre_commit.languages.all import Language +from pre_commit.prefix import Prefix + + +def run_language( + path: os.PathLike[str], + language: Language, + exe: str, + args: Sequence[str] = (), + file_args: Sequence[str] = (), + version: str = C.DEFAULT, + deps: Sequence[str] = (), +) -> tuple[int, bytes]: + prefix = Prefix(str(path)) + + language.install_environment(prefix, version, deps) + with language.in_env(prefix, version): + ret, out = language.run_hook( + prefix, + exe, + args, + file_args, + require_serial=True, + color=False, + ) + out = out.replace(b'\r\n', b'\n') + return ret, out diff --git a/testing/resources/coursier_hooks_repo/.pre-commit-channel/echo-java.json b/testing/resources/coursier_hooks_repo/.pre-commit-channel/echo-java.json deleted file mode 100644 index 37f401e2c..000000000 --- a/testing/resources/coursier_hooks_repo/.pre-commit-channel/echo-java.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "repositories": [ - "central" - ], - "dependencies": [ - "io.get-coursier:echo:latest.stable" - ] -} diff --git a/testing/resources/coursier_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/coursier_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index d4a143b3d..000000000 --- a/testing/resources/coursier_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: echo-java - name: echo-java - description: echo from java - entry: echo-java - language: coursier diff --git a/testing/util.py b/testing/util.py index e807f0482..324f1f6c3 100644 --- a/testing/util.py +++ b/testing/util.py @@ -42,10 +42,6 @@ def cmd_output_mocked_pre_commit_home( return ret, out.replace('\r\n', '\n'), None -skipif_cant_run_coursier = pytest.mark.skipif( - os.name == 'nt' or parse_shebang.find_executable('cs') is None, - reason="coursier isn't installed or can't be found", -) skipif_cant_run_docker = pytest.mark.skipif( os.name == 'nt' or not docker_is_running(), reason="Docker isn't running or can't be accessed", diff --git a/tests/languages/coursier_test.py b/tests/languages/coursier_test.py new file mode 100644 index 000000000..dbb746ca8 --- /dev/null +++ b/tests/languages/coursier_test.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import pytest + +from pre_commit.errors import FatalError +from pre_commit.languages import coursier +from testing.language_helpers import run_language + + +def test_coursier_hook(tmp_path): + echo_java_json = '''\ +{ + "repositories": ["central"], + "dependencies": ["io.get-coursier:echo:latest.stable"] +} +''' + + channel_dir = tmp_path.joinpath('.pre-commit-channel') + channel_dir.mkdir() + channel_dir.joinpath('echo-java.json').write_text(echo_java_json) + + ret = run_language( + tmp_path, + coursier, + 'echo-java', + args=('Hello', 'World', 'from', 'coursier'), + ) + assert ret == (0, b'Hello World from coursier\n') + + +def test_coursier_hook_additional_dependencies(tmp_path): + ret = run_language( + tmp_path, + coursier, + 'scalafmt --version', + deps=('scalafmt:3.6.1',), + ) + assert ret == (0, b'scalafmt 3.6.1\n') + + +def test_error_if_no_deps_or_channel(tmp_path): + with pytest.raises(FatalError) as excinfo: + run_language(tmp_path, coursier, 'dne') + msg, = excinfo.value.args + assert msg == 'expected .pre-commit-channel dir or additional_dependencies' diff --git a/tests/repository_test.py b/tests/repository_test.py index 4043491bc..5e4dff1e0 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -32,7 +32,6 @@ from testing.fixtures import modify_manifest from testing.util import cwd from testing.util import get_resource_path -from testing.util import skipif_cant_run_coursier from testing.util import skipif_cant_run_docker from testing.util import skipif_cant_run_lua from testing.util import skipif_cant_run_swift @@ -199,15 +198,6 @@ def test_language_versioned_python_hook(tempdir_factory, store): ) -@skipif_cant_run_coursier # pragma: win32 no cover -def test_run_a_coursier_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'coursier_hooks_repo', - 'echo-java', - ['Hello World from coursier'], b'Hello World from coursier\n', - ) - - @skipif_cant_run_docker # pragma: win32 no cover def test_run_a_docker_hook(tempdir_factory, store): _test_hook_repo( From f1b5f6637481704b687b2f3bbda49500af7849c1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jan 2023 11:39:48 -0500 Subject: [PATCH 726/967] test conda language directly --- pre_commit/store.py | 40 +++++++++--------- .../conda_hooks_repo/.pre-commit-hooks.yaml | 10 ----- .../conda_hooks_repo/environment.yml | 6 --- tests/languages/conda_test.py | 36 +++++++++++++++- tests/repository_test.py | 41 ------------------- tests/store_test.py | 3 +- 6 files changed, 57 insertions(+), 79 deletions(-) delete mode 100644 testing/resources/conda_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/conda_hooks_repo/environment.yml diff --git a/pre_commit/store.py b/pre_commit/store.py index e42cc4897..6ddc7c481 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -36,6 +36,26 @@ def _get_default_directory() -> str: return os.path.realpath(ret) +_LOCAL_RESOURCES = ( + 'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore', + 'package.json', 'pre-commit-package-dev-1.rockspec', + 'pre_commit_placeholder_package.gemspec', 'setup.py', + 'environment.yml', 'Makefile.PL', 'pubspec.yaml', + 'renv.lock', 'renv/activate.R', 'renv/LICENSE.renv', +) + + +def _make_local_repo(directory: str) -> None: + for resource in _LOCAL_RESOURCES: + resource_dirname, resource_basename = os.path.split(resource) + contents = resource_text(f'empty_template_{resource_basename}') + target_dir = os.path.join(directory, resource_dirname) + target_file = os.path.join(target_dir, resource_basename) + os.makedirs(target_dir, exist_ok=True) + with open(target_file, 'w') as f: + f.write(contents) + + class Store: get_default_directory = staticmethod(_get_default_directory) @@ -185,27 +205,9 @@ def _git_cmd(*args: str) -> None: return self._new_repo(repo, ref, deps, clone_strategy) - LOCAL_RESOURCES = ( - 'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore', - 'package.json', 'pre-commit-package-dev-1.rockspec', - 'pre_commit_placeholder_package.gemspec', 'setup.py', - 'environment.yml', 'Makefile.PL', 'pubspec.yaml', - 'renv.lock', 'renv/activate.R', 'renv/LICENSE.renv', - ) - def make_local(self, deps: Sequence[str]) -> str: - def make_local_strategy(directory: str) -> None: - for resource in self.LOCAL_RESOURCES: - resource_dirname, resource_basename = os.path.split(resource) - contents = resource_text(f'empty_template_{resource_basename}') - target_dir = os.path.join(directory, resource_dirname) - target_file = os.path.join(target_dir, resource_basename) - os.makedirs(target_dir, exist_ok=True) - with open(target_file, 'w') as f: - f.write(contents) - return self._new_repo( - 'local', C.LOCAL_REPO_VERSION, deps, make_local_strategy, + 'local', C.LOCAL_REPO_VERSION, deps, _make_local_repo, ) def _create_config_table(self, db: sqlite3.Connection) -> None: diff --git a/testing/resources/conda_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/conda_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index a0d274c23..000000000 --- a/testing/resources/conda_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- id: sys-exec - name: sys-exec - entry: python -c 'import os; import sys; print(sys.executable.split(os.path.sep)[-2]) if os.name == "nt" else print(sys.executable.split(os.path.sep)[-3])' - language: conda - files: \.py$ -- id: additional-deps - name: additional-deps - entry: python - language: conda - files: \.py$ diff --git a/testing/resources/conda_hooks_repo/environment.yml b/testing/resources/conda_hooks_repo/environment.yml deleted file mode 100644 index e23c079fd..000000000 --- a/testing/resources/conda_hooks_repo/environment.yml +++ /dev/null @@ -1,6 +0,0 @@ -channels: - - conda-forge - - defaults -dependencies: - - python - - pip diff --git a/tests/languages/conda_test.py b/tests/languages/conda_test.py index 5023b2afb..83aaebed3 100644 --- a/tests/languages/conda_test.py +++ b/tests/languages/conda_test.py @@ -1,9 +1,13 @@ from __future__ import annotations +import os.path + import pytest from pre_commit import envcontext -from pre_commit.languages.conda import _conda_exe +from pre_commit.languages import conda +from pre_commit.store import _make_local_repo +from testing.language_helpers import run_language @pytest.mark.parametrize( @@ -37,4 +41,32 @@ ) def test_conda_exe(ctx, expected): with envcontext.envcontext(ctx): - assert _conda_exe() == expected + assert conda._conda_exe() == expected + + +def test_conda_language(tmp_path): + environment_yml = '''\ +channels: [conda-forge, defaults] +dependencies: [python, pip] +''' + tmp_path.joinpath('environment.yml').write_text(environment_yml) + + ret, out = run_language( + tmp_path, + conda, + 'python -c "import sys; print(sys.prefix)"', + ) + assert ret == 0 + assert os.path.basename(out.strip()) == b'conda-default' + + +def test_conda_additional_deps(tmp_path): + _make_local_repo(tmp_path) + + ret = run_language( + tmp_path, + conda, + 'python -c "import botocore; print(1)"', + deps=('botocore',), + ) + assert ret == (0, b'1\n') diff --git a/tests/repository_test.py b/tests/repository_test.py index 5e4dff1e0..0bf27967d 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -88,47 +88,6 @@ def _test_hook_repo( assert _norm_out(out) == expected -def test_conda_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'conda_hooks_repo', - 'sys-exec', [os.devnull], - b'conda-default\n', - ) - - -def test_conda_with_additional_dependencies_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'conda_hooks_repo', - 'additional-deps', [os.devnull], - b'OK\n', - config_kwargs={ - 'hooks': [{ - 'id': 'additional-deps', - 'args': ['-c', 'import tzdata; print("OK")'], - 'additional_dependencies': ['python-tzdata'], - }], - }, - ) - - -def test_local_conda_additional_dependencies(store): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'local-conda', - 'name': 'local-conda', - 'entry': 'python', - 'language': 'conda', - 'args': ['-c', 'import botocore; print("OK")'], - 'additional_dependencies': ['botocore'], - }], - } - hook = _get_hook(config, store, 'local-conda') - ret, out = _hook_run(hook, (), color=False) - assert ret == 0 - assert _norm_out(out) == b'OK\n' - - def test_python_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'python_hooks_repo', diff --git a/tests/store_test.py b/tests/store_test.py index 818776623..c42ce6537 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -9,6 +9,7 @@ from pre_commit import git from pre_commit.store import _get_default_directory +from pre_commit.store import _LOCAL_RESOURCES from pre_commit.store import Store from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output @@ -188,7 +189,7 @@ def test_local_resources_reflects_reality(): for res in os.listdir('pre_commit/resources') if res.startswith('empty_template_') } - assert on_disk == {os.path.basename(x) for x in Store.LOCAL_RESOURCES} + assert on_disk == {os.path.basename(x) for x in _LOCAL_RESOURCES} def test_mark_config_as_used(store, tmpdir): From c36f03cd2e8ae948b35516affa8a4b71c6fd3289 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 16 Jan 2023 17:10:58 -0500 Subject: [PATCH 727/967] test swift language directly --- testing/resources/swift_hooks_repo/.gitignore | 4 --- .../swift_hooks_repo/.pre-commit-hooks.yaml | 6 ---- .../resources/swift_hooks_repo/Package.swift | 7 ----- .../Sources/swift_hooks_repo/main.swift | 1 - testing/util.py | 5 --- tests/languages/swift_test.py | 31 +++++++++++++++++++ tests/repository_test.py | 9 ------ 7 files changed, 31 insertions(+), 32 deletions(-) delete mode 100644 testing/resources/swift_hooks_repo/.gitignore delete mode 100644 testing/resources/swift_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/swift_hooks_repo/Package.swift delete mode 100644 testing/resources/swift_hooks_repo/Sources/swift_hooks_repo/main.swift create mode 100644 tests/languages/swift_test.py diff --git a/testing/resources/swift_hooks_repo/.gitignore b/testing/resources/swift_hooks_repo/.gitignore deleted file mode 100644 index 02c087533..000000000 --- a/testing/resources/swift_hooks_repo/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -.DS_Store -/.build -/Packages -/*.xcodeproj diff --git a/testing/resources/swift_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/swift_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index c08df87d4..000000000 --- a/testing/resources/swift_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,6 +0,0 @@ -- id: swift-hooks-repo - name: Swift hooks repo example - description: Runs the hello world app generated by swift package init --type executable (binary called swift_hooks_repo here) - entry: swift_hooks_repo - language: swift - files: \.(swift)$ diff --git a/testing/resources/swift_hooks_repo/Package.swift b/testing/resources/swift_hooks_repo/Package.swift deleted file mode 100644 index 04976d3ff..000000000 --- a/testing/resources/swift_hooks_repo/Package.swift +++ /dev/null @@ -1,7 +0,0 @@ -// swift-tools-version:5.0 -import PackageDescription - -let package = Package( - name: "swift_hooks_repo", - targets: [.target(name: "swift_hooks_repo")] -) diff --git a/testing/resources/swift_hooks_repo/Sources/swift_hooks_repo/main.swift b/testing/resources/swift_hooks_repo/Sources/swift_hooks_repo/main.swift deleted file mode 100644 index f7cf60e14..000000000 --- a/testing/resources/swift_hooks_repo/Sources/swift_hooks_repo/main.swift +++ /dev/null @@ -1 +0,0 @@ -print("Hello, world!") diff --git a/testing/util.py b/testing/util.py index 324f1f6c3..a5ae06d05 100644 --- a/testing/util.py +++ b/testing/util.py @@ -6,7 +6,6 @@ import pytest -from pre_commit import parse_shebang from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b @@ -50,10 +49,6 @@ def cmd_output_mocked_pre_commit_home( os.name == 'nt', reason="lua isn't installed or can't be found", ) -skipif_cant_run_swift = pytest.mark.skipif( - parse_shebang.find_executable('swift') is None, - reason="swift isn't installed or can't be found", -) xfailif_windows = pytest.mark.xfail(os.name == 'nt', reason='windows') diff --git a/tests/languages/swift_test.py b/tests/languages/swift_test.py new file mode 100644 index 000000000..e0a8ea425 --- /dev/null +++ b/tests/languages/swift_test.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +import sys + +import pytest + +from pre_commit.languages import swift +from testing.language_helpers import run_language + + +@pytest.mark.skipif( + sys.platform == 'win32', + reason='swift is not supported on windows', +) +def test_swift_language(tmp_path): # pragma: win32 no cover + package_swift = '''\ +// swift-tools-version:5.0 +import PackageDescription + +let package = Package( + name: "swift_hooks_repo", + targets: [.target(name: "swift_hooks_repo")] +) +''' + tmp_path.joinpath('Package.swift').write_text(package_swift) + src_dir = tmp_path.joinpath('Sources/swift_hooks_repo') + src_dir.mkdir(parents=True) + src_dir.joinpath('main.swift').write_text('print("Hello, world!")\n') + + expected = (0, b'Hello, world!\n') + assert run_language(tmp_path, swift, 'swift_hooks_repo') == expected diff --git a/tests/repository_test.py b/tests/repository_test.py index 0bf27967d..fc2769849 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -34,7 +34,6 @@ from testing.util import get_resource_path from testing.util import skipif_cant_run_docker from testing.util import skipif_cant_run_lua -from testing.util import skipif_cant_run_swift from testing.util import xfailif_windows @@ -329,14 +328,6 @@ def test_system_hook_with_spaces(tempdir_factory, store): ) -@skipif_cant_run_swift # pragma: win32 no cover -def test_swift_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'swift_hooks_repo', - 'swift-hooks-repo', [], b'Hello, world!\n', - ) - - def test_golang_system_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'golang_hooks_repo', From 966c67a8321e301d844f776cb438c4b5808abbc6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jan 2023 14:16:13 -0500 Subject: [PATCH 728/967] speed up R unit tests --- pre_commit/languages/r.py | 2 +- .../r_hooks_repo/.pre-commit-hooks.yaml | 23 ---- tests/languages/r_test.py | 121 +++++++----------- 3 files changed, 46 insertions(+), 100 deletions(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 7ed3eafcd..dc3986057 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -64,7 +64,7 @@ def _entry_validate(entry: list[str]) -> None: raise ValueError('You can supply at most one expression.') elif len(entry) > 2: raise ValueError( - 'The only valid syntax is `Rscript -e {expr}`', + 'The only valid syntax is `Rscript -e {expr}`' 'or `Rscript path/to/hook/script`', ) diff --git a/testing/resources/r_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/r_hooks_repo/.pre-commit-hooks.yaml index b3545d969..ef46cc0a2 100644 --- a/testing/resources/r_hooks_repo/.pre-commit-hooks.yaml +++ b/testing/resources/r_hooks_repo/.pre-commit-hooks.yaml @@ -1,26 +1,3 @@ -# parsing file -- id: parse-file-no-opts-no-args - name: Say hi - entry: Rscript parse-file-no-opts-no-args.R - language: r - types: [r] -- id: parse-file-no-opts-args - name: Say hi - entry: Rscript parse-file-no-opts-args.R - args: [--no-cache] - language: r - types: [r] -## parsing expr -- id: parse-expr-no-opts-no-args-1 - name: Say hi - entry: Rscript -e '1+1' - language: r - types: [r] -- id: parse-expr-args-in-entry-2 - name: Say hi - entry: Rscript -e '1+1' -e '3' --no-cache3 - language: r - types: [r] # real world - id: hello-world name: Say hi diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py index d23441401..0c5e56382 100644 --- a/tests/languages/r_test.py +++ b/tests/languages/r_test.py @@ -6,117 +6,86 @@ from pre_commit import envcontext from pre_commit.languages import r +from pre_commit.prefix import Prefix from pre_commit.util import win_exe -from testing.fixtures import make_config_from_repo -from testing.fixtures import make_repo -from tests.repository_test import _get_hook_no_install - - -def _test_r_parsing( - tempdir_factory, - store, - hook_id, - expected_hook_expr=(), - expected_args=(), - config=None, -): - repo = make_repo(tempdir_factory, 'r_hooks_repo') - config = make_config_from_repo(repo) - hook = _get_hook_no_install(config, store, hook_id) - ret = r._cmd_from_hook(hook.prefix, hook.entry, hook.args) - expected_path = os.path.join(hook.prefix.prefix_dir, f'{hook_id}.R') - expected = ( + + +def test_r_parsing_file_no_opts_no_args(tmp_path): + cmd = r._cmd_from_hook(Prefix(str(tmp_path)), 'Rscript some-script.R', ()) + assert cmd == ( 'Rscript', '--no-save', '--no-restore', '--no-site-file', '--no-environ', - *(expected_hook_expr or (expected_path,)), - *expected_args, + str(tmp_path.joinpath('some-script.R')), ) - assert ret == expected - - -def test_r_parsing_file_no_opts_no_args(tempdir_factory, store): - hook_id = 'parse-file-no-opts-no-args' - _test_r_parsing(tempdir_factory, store, hook_id) -def test_r_parsing_file_opts_no_args(tempdir_factory, store): +def test_r_parsing_file_opts_no_args(): with pytest.raises(ValueError) as excinfo: r._entry_validate(['Rscript', '--no-init', '/path/to/file']) - msg = excinfo.value.args + msg, = excinfo.value.args assert msg == ( - 'The only valid syntax is `Rscript -e {expr}`', - 'or `Rscript path/to/hook/script`', + 'The only valid syntax is `Rscript -e {expr}`' + 'or `Rscript path/to/hook/script`' ) -def test_r_parsing_file_no_opts_args(tempdir_factory, store): - hook_id = 'parse-file-no-opts-args' - expected_args = ['--no-cache'] - _test_r_parsing( - tempdir_factory, store, hook_id, expected_args=expected_args, +def test_r_parsing_file_no_opts_args(tmp_path): + cmd = r._cmd_from_hook( + Prefix(str(tmp_path)), + 'Rscript some-script.R', + ('--no-cache',), + ) + assert cmd == ( + 'Rscript', + '--no-save', '--no-restore', '--no-site-file', '--no-environ', + str(tmp_path.joinpath('some-script.R')), + '--no-cache', ) -def test_r_parsing_expr_no_opts_no_args1(tempdir_factory, store): - hook_id = 'parse-expr-no-opts-no-args-1' - _test_r_parsing( - tempdir_factory, store, hook_id, expected_hook_expr=('-e', '1+1'), +def test_r_parsing_expr_no_opts_no_args1(tmp_path): + cmd = r._cmd_from_hook(Prefix(str(tmp_path)), "Rscript -e '1+1'", ()) + assert cmd == ( + 'Rscript', + '--no-save', '--no-restore', '--no-site-file', '--no-environ', + '-e', '1+1', ) -def test_r_parsing_expr_no_opts_no_args2(tempdir_factory, store): - with pytest.raises(ValueError) as execinfo: +def test_r_parsing_expr_no_opts_no_args2(): + with pytest.raises(ValueError) as excinfo: r._entry_validate(['Rscript', '-e', '1+1', '-e', 'letters']) - msg = execinfo.value.args - assert msg == ('You can supply at most one expression.',) + msg, = excinfo.value.args + assert msg == 'You can supply at most one expression.' -def test_r_parsing_expr_opts_no_args2(tempdir_factory, store): - with pytest.raises(ValueError) as execinfo: +def test_r_parsing_expr_opts_no_args2(): + with pytest.raises(ValueError) as excinfo: r._entry_validate( ['Rscript', '--vanilla', '-e', '1+1', '-e', 'letters'], ) - msg = execinfo.value.args + msg, = excinfo.value.args assert msg == ( - 'The only valid syntax is `Rscript -e {expr}`', - 'or `Rscript path/to/hook/script`', + 'The only valid syntax is `Rscript -e {expr}`' + 'or `Rscript path/to/hook/script`' ) -def test_r_parsing_expr_args_in_entry2(tempdir_factory, store): - with pytest.raises(ValueError) as execinfo: +def test_r_parsing_expr_args_in_entry2(): + with pytest.raises(ValueError) as excinfo: r._entry_validate(['Rscript', '-e', 'expr1', '--another-arg']) - msg = execinfo.value.args - assert msg == ('You can supply at most one expression.',) + msg, = excinfo.value.args + assert msg == 'You can supply at most one expression.' -def test_r_parsing_expr_non_Rscirpt(tempdir_factory, store): - with pytest.raises(ValueError) as execinfo: +def test_r_parsing_expr_non_Rscirpt(): + with pytest.raises(ValueError) as excinfo: r._entry_validate(['AnotherScript', '-e', '{{}}']) - msg = execinfo.value.args - assert msg == ('entry must start with `Rscript`.',) - - -def test_r_parsing_file_local(tempdir_factory, store): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'local-r', - 'name': 'local-r', - 'entry': 'Rscript path/to/script.R', - 'language': 'r', - }], - } - hook = _get_hook_no_install(config, store, 'local-r') - ret = r._cmd_from_hook(hook.prefix, hook.entry, hook.args) - assert ret == ( - 'Rscript', - '--no-save', '--no-restore', '--no-site-file', '--no-environ', - hook.prefix.path('path/to/script.R'), - ) + msg, = excinfo.value.args + assert msg == 'entry must start with `Rscript`.' def test_rscript_exec_relative_to_r_home(): From d24055cb40a4473754cb7560408a2c15544b387b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jan 2023 17:34:04 -0500 Subject: [PATCH 729/967] test perl language directly --- testing/resources/perl_hooks_repo/.gitignore | 7 -- .../perl_hooks_repo/.pre-commit-hooks.yaml | 5 -- testing/resources/perl_hooks_repo/MANIFEST | 4 -- testing/resources/perl_hooks_repo/Makefile.PL | 10 --- .../perl_hooks_repo/bin/pre-commit-perl-hello | 7 -- .../perl_hooks_repo/lib/PreCommitHello.pm | 12 ---- tests/languages/perl_test.py | 69 +++++++++++++++++++ tests/repository_test.py | 24 ------- 8 files changed, 69 insertions(+), 69 deletions(-) delete mode 100644 testing/resources/perl_hooks_repo/.gitignore delete mode 100644 testing/resources/perl_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/perl_hooks_repo/MANIFEST delete mode 100644 testing/resources/perl_hooks_repo/Makefile.PL delete mode 100755 testing/resources/perl_hooks_repo/bin/pre-commit-perl-hello delete mode 100644 testing/resources/perl_hooks_repo/lib/PreCommitHello.pm create mode 100644 tests/languages/perl_test.py diff --git a/testing/resources/perl_hooks_repo/.gitignore b/testing/resources/perl_hooks_repo/.gitignore deleted file mode 100644 index 7af994045..000000000 --- a/testing/resources/perl_hooks_repo/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -/MYMETA.json -/MYMETA.yml -/Makefile -/PreCommitHello-*.tar.* -/PreCommitHello-*/ -/blib/ -/pm_to_blib diff --git a/testing/resources/perl_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/perl_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 11e6f6cd9..000000000 --- a/testing/resources/perl_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: perl-hook - name: perl example hook - entry: pre-commit-perl-hello - language: perl - files: '' diff --git a/testing/resources/perl_hooks_repo/MANIFEST b/testing/resources/perl_hooks_repo/MANIFEST deleted file mode 100644 index 4a20084c6..000000000 --- a/testing/resources/perl_hooks_repo/MANIFEST +++ /dev/null @@ -1,4 +0,0 @@ -MANIFEST -Makefile.PL -bin/pre-commit-perl-hello -lib/PreCommitHello.pm diff --git a/testing/resources/perl_hooks_repo/Makefile.PL b/testing/resources/perl_hooks_repo/Makefile.PL deleted file mode 100644 index 6c70e1071..000000000 --- a/testing/resources/perl_hooks_repo/Makefile.PL +++ /dev/null @@ -1,10 +0,0 @@ -use strict; -use warnings; - -use ExtUtils::MakeMaker; - -WriteMakefile( - NAME => "PreCommitHello", - VERSION_FROM => "lib/PreCommitHello.pm", - EXE_FILES => [qw(bin/pre-commit-perl-hello)], -); diff --git a/testing/resources/perl_hooks_repo/bin/pre-commit-perl-hello b/testing/resources/perl_hooks_repo/bin/pre-commit-perl-hello deleted file mode 100755 index 9474009a1..000000000 --- a/testing/resources/perl_hooks_repo/bin/pre-commit-perl-hello +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env perl - -use strict; -use warnings; -use PreCommitHello; - -PreCommitHello::hello(); diff --git a/testing/resources/perl_hooks_repo/lib/PreCommitHello.pm b/testing/resources/perl_hooks_repo/lib/PreCommitHello.pm deleted file mode 100644 index c76521cea..000000000 --- a/testing/resources/perl_hooks_repo/lib/PreCommitHello.pm +++ /dev/null @@ -1,12 +0,0 @@ -package PreCommitHello; - -use strict; -use warnings; - -our $VERSION = "0.1.0"; - -sub hello { - print "Hello from perl-commit Perl!\n"; -} - -1; diff --git a/tests/languages/perl_test.py b/tests/languages/perl_test.py new file mode 100644 index 000000000..042478dbb --- /dev/null +++ b/tests/languages/perl_test.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from pre_commit.languages import perl +from pre_commit.store import _make_local_repo +from pre_commit.util import make_executable +from testing.language_helpers import run_language + + +def test_perl_install(tmp_path): + makefile_pl = '''\ +use strict; +use warnings; + +use ExtUtils::MakeMaker; + +WriteMakefile( + NAME => "PreCommitHello", + VERSION_FROM => "lib/PreCommitHello.pm", + EXE_FILES => [qw(bin/pre-commit-perl-hello)], +); +''' + bin_perl_hello = '''\ +#!/usr/bin/env perl + +use strict; +use warnings; +use PreCommitHello; + +PreCommitHello::hello(); +''' + lib_hello_pm = '''\ +package PreCommitHello; + +use strict; +use warnings; + +our $VERSION = "0.1.0"; + +sub hello { + print "Hello from perl-commit Perl!\n"; +} + +1; +''' + tmp_path.joinpath('Makefile.PL').write_text(makefile_pl) + bin_dir = tmp_path.joinpath('bin') + bin_dir.mkdir() + exe = bin_dir.joinpath('pre-commit-perl-hello') + exe.write_text(bin_perl_hello) + make_executable(exe) + lib_dir = tmp_path.joinpath('lib') + lib_dir.mkdir() + lib_dir.joinpath('PreCommitHello.pm').write_text(lib_hello_pm) + + ret = run_language(tmp_path, perl, 'pre-commit-perl-hello') + assert ret == (0, b'Hello from perl-commit Perl!\n') + + +def test_perl_additional_dependencies(tmp_path): + _make_local_repo(str(tmp_path)) + + ret, out = run_language( + tmp_path, + perl, + 'perltidy --version', + deps=('SHANCOCK/Perl-Tidy-20211029.tar.gz',), + ) + assert ret == 0 + assert out.startswith(b'This is perltidy, v20211029') diff --git a/tests/repository_test.py b/tests/repository_test.py index fc2769849..2389c4483 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -981,30 +981,6 @@ def test_manifest_hooks(tempdir_factory, store): ) -def test_perl_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'perl_hooks_repo', - 'perl-hook', [], b'Hello from perl-commit Perl!\n', - ) - - -def test_local_perl_additional_dependencies(store): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'hello', - 'name': 'hello', - 'entry': 'perltidy --version', - 'language': 'perl', - 'additional_dependencies': ['SHANCOCK/Perl-Tidy-20211029.tar.gz'], - }], - } - hook = _get_hook(config, store, 'hello') - ret, out = _hook_run(hook, (), color=False) - assert ret == 0 - assert _norm_out(out).startswith(b'This is perltidy, v20211029') - - @pytest.mark.parametrize( 'repo', ( From 043565d28a0cccda9892baa414ee52c2f5b61372 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jan 2023 18:44:14 -0500 Subject: [PATCH 730/967] test dart directly --- .../dart_repo/.pre-commit-hooks.yaml | 4 -- .../dart_repo/bin/hello-world-dart.dart | 6 -- testing/resources/dart_repo/pubspec.yaml | 10 --- tests/languages/dart_test.py | 62 +++++++++++++++++++ tests/repository_test.py | 40 ------------ 5 files changed, 62 insertions(+), 60 deletions(-) delete mode 100644 testing/resources/dart_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/dart_repo/bin/hello-world-dart.dart delete mode 100644 testing/resources/dart_repo/pubspec.yaml create mode 100644 tests/languages/dart_test.py diff --git a/testing/resources/dart_repo/.pre-commit-hooks.yaml b/testing/resources/dart_repo/.pre-commit-hooks.yaml deleted file mode 100644 index e0dc5a2a9..000000000 --- a/testing/resources/dart_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,4 +0,0 @@ -- id: hello-world-dart - name: hello world dart - entry: hello-world-dart - language: dart diff --git a/testing/resources/dart_repo/bin/hello-world-dart.dart b/testing/resources/dart_repo/bin/hello-world-dart.dart deleted file mode 100644 index 5d8d6a6af..000000000 --- a/testing/resources/dart_repo/bin/hello-world-dart.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:ansicolor/ansicolor.dart'; - -void main() { - AnsiPen pen = new AnsiPen()..red(); - print("hello hello " + pen("world")); -} diff --git a/testing/resources/dart_repo/pubspec.yaml b/testing/resources/dart_repo/pubspec.yaml deleted file mode 100644 index bc719d055..000000000 --- a/testing/resources/dart_repo/pubspec.yaml +++ /dev/null @@ -1,10 +0,0 @@ -environment: - sdk: '>=2.10.0 <3.0.0' - -name: hello_world_dart - -executables: - hello-world-dart: - -dependencies: - ansicolor: ^2.0.1 diff --git a/tests/languages/dart_test.py b/tests/languages/dart_test.py new file mode 100644 index 000000000..5bb5aa68f --- /dev/null +++ b/tests/languages/dart_test.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +import re_assert + +from pre_commit.languages import dart +from pre_commit.store import _make_local_repo +from testing.language_helpers import run_language + + +def test_dart(tmp_path): + pubspec_yaml = '''\ +environment: + sdk: '>=2.10.0 <3.0.0' + +name: hello_world_dart + +executables: + hello-world-dart: + +dependencies: + ansicolor: ^2.0.1 +''' + hello_world_dart_dart = '''\ +import 'package:ansicolor/ansicolor.dart'; + +void main() { + AnsiPen pen = new AnsiPen()..red(); + print("hello hello " + pen("world")); +} +''' + tmp_path.joinpath('pubspec.yaml').write_text(pubspec_yaml) + bin_dir = tmp_path.joinpath('bin') + bin_dir.mkdir() + bin_dir.joinpath('hello-world-dart.dart').write_text(hello_world_dart_dart) + + expected = (0, b'hello hello world\n') + assert run_language(tmp_path, dart, 'hello-world-dart') == expected + + +def test_dart_additional_deps(tmp_path): + _make_local_repo(str(tmp_path)) + + ret = run_language( + tmp_path, + dart, + 'hello-world-dart', + deps=('hello_world_dart',), + ) + assert ret == (0, b'hello hello world\n') + + +def test_dart_additional_deps_versioned(tmp_path): + _make_local_repo(str(tmp_path)) + + ret, out = run_language( + tmp_path, + dart, + 'secure-random -l 4 -b 16', + deps=('encrypt:5.0.0',), + ) + assert ret == 0 + re_assert.Matches('^[a-f0-9]{8}\n$').assert_matches(out.decode()) diff --git a/tests/repository_test.py b/tests/repository_test.py index 2389c4483..0d01f0f65 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -997,46 +997,6 @@ def test_dotnet_hook(tempdir_factory, store, repo): ) -def test_dart_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'dart_repo', - 'hello-world-dart', [], b'hello hello world\n', - ) - - -def test_local_dart_additional_dependencies(store): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'local-dart', - 'name': 'local-dart', - 'entry': 'hello-world-dart', - 'language': 'dart', - 'additional_dependencies': ['hello_world_dart'], - }], - } - hook = _get_hook(config, store, 'local-dart') - ret, out = _hook_run(hook, (), color=False) - assert (ret, _norm_out(out)) == (0, b'hello hello world\n') - - -def test_local_dart_additional_dependencies_versioned(store): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'local-dart', - 'name': 'local-dart', - 'entry': 'secure-random -l 4 -b 16', - 'language': 'dart', - 'additional_dependencies': ['encrypt:5.0.0'], - }], - } - hook = _get_hook(config, store, 'local-dart') - ret, out = _hook_run(hook, (), color=False) - assert ret == 0 - re_assert.Matches('^[a-f0-9]{8}\r?\n$').assert_matches(out.decode()) - - def test_non_installable_hook_error_for_language_version(store, caplog): config = { 'repo': 'local', From 7512e3b7e1d367464a3b8acad63166a1e55119d1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jan 2023 23:25:00 -0500 Subject: [PATCH 731/967] test r language directly --- .../r_hooks_repo/.pre-commit-hooks.yaml | 25 - testing/resources/r_hooks_repo/DESCRIPTION | 19 - .../resources/r_hooks_repo/additional-deps.R | 2 - testing/resources/r_hooks_repo/hello-world.R | 5 - testing/resources/r_hooks_repo/renv.lock | 27 -- testing/resources/r_hooks_repo/renv/LICENSE | 7 - .../resources/r_hooks_repo/renv/activate.R | 440 ------------------ tests/languages/r_test.py | 99 ++++ tests/repository_test.py | 48 -- 9 files changed, 99 insertions(+), 573 deletions(-) delete mode 100644 testing/resources/r_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/r_hooks_repo/DESCRIPTION delete mode 100755 testing/resources/r_hooks_repo/additional-deps.R delete mode 100755 testing/resources/r_hooks_repo/hello-world.R delete mode 100644 testing/resources/r_hooks_repo/renv.lock delete mode 100644 testing/resources/r_hooks_repo/renv/LICENSE delete mode 100644 testing/resources/r_hooks_repo/renv/activate.R diff --git a/testing/resources/r_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/r_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index ef46cc0a2..000000000 --- a/testing/resources/r_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# real world -- id: hello-world - name: Say hi - entry: Rscript hello-world.R - args: [blibla] - language: r - types: [r] -- id: hello-world-inline - name: Say hi - entry: | - Rscript -e - 'stopifnot( - packageVersion("rprojroot") == "1.0", - packageVersion("gli.clu") == "0.0.0.9000" - ) - cat(commandArgs(trailingOnly = TRUE), "from R!\n", sep = ", ") - ' - args: ['Hi-there'] - language: r - types: [r] -- id: additional-deps - name: Check additional deps - entry: Rscript additional-deps.R - language: r - types: [r] diff --git a/testing/resources/r_hooks_repo/DESCRIPTION b/testing/resources/r_hooks_repo/DESCRIPTION deleted file mode 100644 index 0e597a8a6..000000000 --- a/testing/resources/r_hooks_repo/DESCRIPTION +++ /dev/null @@ -1,19 +0,0 @@ -Package: gli.clu -Title: What the Package Does (One Line, Title Case) -Type: Package -Version: 0.0.0.9000 -Authors@R: - person(given = "First", - family = "Last", - role = c("aut", "cre"), - email = "first.last@example.com", - comment = c(ORCID = "YOUR-ORCID-ID")) -Description: What the package does (one paragraph). -License: `use_mit_license()`, `use_gpl3_license()` or friends to - pick a license -Encoding: UTF-8 -LazyData: true -Roxygen: list(markdown = TRUE) -RoxygenNote: 7.1.1 -Imports: - rprojroot diff --git a/testing/resources/r_hooks_repo/additional-deps.R b/testing/resources/r_hooks_repo/additional-deps.R deleted file mode 100755 index bc145951b..000000000 --- a/testing/resources/r_hooks_repo/additional-deps.R +++ /dev/null @@ -1,2 +0,0 @@ -suppressPackageStartupMessages(library("cachem")) -cat("OK\n") diff --git a/testing/resources/r_hooks_repo/hello-world.R b/testing/resources/r_hooks_repo/hello-world.R deleted file mode 100755 index bf8d92f42..000000000 --- a/testing/resources/r_hooks_repo/hello-world.R +++ /dev/null @@ -1,5 +0,0 @@ -stopifnot( - packageVersion('rprojroot') == '1.0', - packageVersion('gli.clu') == '0.0.0.9000' -) -cat("Hello, World, from R!\n") diff --git a/testing/resources/r_hooks_repo/renv.lock b/testing/resources/r_hooks_repo/renv.lock deleted file mode 100644 index d7d5fdcc9..000000000 --- a/testing/resources/r_hooks_repo/renv.lock +++ /dev/null @@ -1,27 +0,0 @@ -{ - "R": { - "Version": "4.0.3", - "Repositories": [ - { - "Name": "CRAN", - "URL": "https://cloud.r-project.org" - } - ] - }, - "Packages": { - "renv": { - "Package": "renv", - "Version": "0.12.5", - "Source": "Repository", - "Repository": "CRAN", - "Hash": "5c0cdb37f063c58cdab3c7e9fbb8bd2c" - }, - "rprojroot": { - "Package": "rprojroot", - "Version": "1.0", - "Source": "Repository", - "Repository": "CRAN", - "Hash": "86704667fe0860e4fec35afdfec137f3" - } - } -} diff --git a/testing/resources/r_hooks_repo/renv/LICENSE b/testing/resources/r_hooks_repo/renv/LICENSE deleted file mode 100644 index 253c5d1ab..000000000 --- a/testing/resources/r_hooks_repo/renv/LICENSE +++ /dev/null @@ -1,7 +0,0 @@ -Copyright 2021 RStudio, PBC - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/testing/resources/r_hooks_repo/renv/activate.R b/testing/resources/r_hooks_repo/renv/activate.R deleted file mode 100644 index d8d092cc6..000000000 --- a/testing/resources/r_hooks_repo/renv/activate.R +++ /dev/null @@ -1,440 +0,0 @@ - -local({ - - # the requested version of renv - version <- "0.12.5" - - # the project directory - project <- getwd() - - # avoid recursion - if (!is.na(Sys.getenv("RENV_R_INITIALIZING", unset = NA))) - return(invisible(TRUE)) - - # signal that we're loading renv during R startup - Sys.setenv("RENV_R_INITIALIZING" = "true") - on.exit(Sys.unsetenv("RENV_R_INITIALIZING"), add = TRUE) - - # signal that we've consented to use renv - options(renv.consent = TRUE) - - # load the 'utils' package eagerly -- this ensures that renv shims, which - # mask 'utils' packages, will come first on the search path - library(utils, lib.loc = .Library) - - # check to see if renv has already been loaded - if ("renv" %in% loadedNamespaces()) { - - # if renv has already been loaded, and it's the requested version of renv, - # nothing to do - spec <- .getNamespaceInfo(.getNamespace("renv"), "spec") - if (identical(spec[["version"]], version)) - return(invisible(TRUE)) - - # otherwise, unload and attempt to load the correct version of renv - unloadNamespace("renv") - - } - - # load bootstrap tools - bootstrap <- function(version, library) { - - # attempt to download renv - tarball <- tryCatch(renv_bootstrap_download(version), error = identity) - if (inherits(tarball, "error")) - stop("failed to download renv ", version) - - # now attempt to install - status <- tryCatch(renv_bootstrap_install(version, tarball, library), error = identity) - if (inherits(status, "error")) - stop("failed to install renv ", version) - - } - - renv_bootstrap_tests_running <- function() { - getOption("renv.tests.running", default = FALSE) - } - - renv_bootstrap_repos <- function() { - - # check for repos override - repos <- Sys.getenv("RENV_CONFIG_REPOS_OVERRIDE", unset = NA) - if (!is.na(repos)) - return(repos) - - # if we're testing, re-use the test repositories - if (renv_bootstrap_tests_running()) - return(getOption("renv.tests.repos")) - - # retrieve current repos - repos <- getOption("repos") - - # ensure @CRAN@ entries are resolved - repos[repos == "@CRAN@"] <- "https://cloud.r-project.org" - - # add in renv.bootstrap.repos if set - default <- c(CRAN = "https://cloud.r-project.org") - extra <- getOption("renv.bootstrap.repos", default = default) - repos <- c(repos, extra) - - # remove duplicates that might've snuck in - dupes <- duplicated(repos) | duplicated(names(repos)) - repos[!dupes] - - } - - renv_bootstrap_download <- function(version) { - - # if the renv version number has 4 components, assume it must - # be retrieved via github - nv <- numeric_version(version) - components <- unclass(nv)[[1]] - - methods <- if (length(components) == 4L) { - list( - renv_bootstrap_download_github - ) - } else { - list( - renv_bootstrap_download_cran_latest, - renv_bootstrap_download_cran_archive - ) - } - - for (method in methods) { - path <- tryCatch(method(version), error = identity) - if (is.character(path) && file.exists(path)) - return(path) - } - - stop("failed to download renv ", version) - - } - - renv_bootstrap_download_impl <- function(url, destfile) { - - mode <- "wb" - - # https://bugs.r-project.org/bugzilla/show_bug.cgi?id=17715 - fixup <- - Sys.info()[["sysname"]] == "Windows" && - substring(url, 1L, 5L) == "file:" - - if (fixup) - mode <- "w+b" - - utils::download.file( - url = url, - destfile = destfile, - mode = mode, - quiet = TRUE - ) - - } - - renv_bootstrap_download_cran_latest <- function(version) { - - repos <- renv_bootstrap_download_cran_latest_find(version) - - message("* Downloading renv ", version, " from CRAN ... ", appendLF = FALSE) - - info <- tryCatch( - utils::download.packages( - pkgs = "renv", - repos = repos, - destdir = tempdir(), - quiet = TRUE - ), - condition = identity - ) - - if (inherits(info, "condition")) { - message("FAILED") - return(FALSE) - } - - message("OK") - info[1, 2] - - } - - renv_bootstrap_download_cran_latest_find <- function(version) { - - all <- renv_bootstrap_repos() - - for (repos in all) { - - db <- tryCatch( - as.data.frame( - x = utils::available.packages(repos = repos), - stringsAsFactors = FALSE - ), - error = identity - ) - - if (inherits(db, "error")) - next - - entry <- db[db$Package %in% "renv" & db$Version %in% version, ] - if (nrow(entry) == 0) - next - - return(repos) - - } - - fmt <- "renv %s is not available from your declared package repositories" - stop(sprintf(fmt, version)) - - } - - renv_bootstrap_download_cran_archive <- function(version) { - - name <- sprintf("renv_%s.tar.gz", version) - repos <- renv_bootstrap_repos() - urls <- file.path(repos, "src/contrib/Archive/renv", name) - destfile <- file.path(tempdir(), name) - - message("* Downloading renv ", version, " from CRAN archive ... ", appendLF = FALSE) - - for (url in urls) { - - status <- tryCatch( - renv_bootstrap_download_impl(url, destfile), - condition = identity - ) - - if (identical(status, 0L)) { - message("OK") - return(destfile) - } - - } - - message("FAILED") - return(FALSE) - - } - - renv_bootstrap_download_github <- function(version) { - - enabled <- Sys.getenv("RENV_BOOTSTRAP_FROM_GITHUB", unset = "TRUE") - if (!identical(enabled, "TRUE")) - return(FALSE) - - # prepare download options - pat <- Sys.getenv("GITHUB_PAT") - if (nzchar(Sys.which("curl")) && nzchar(pat)) { - fmt <- "--location --fail --header \"Authorization: token %s\"" - extra <- sprintf(fmt, pat) - saved <- options("download.file.method", "download.file.extra") - options(download.file.method = "curl", download.file.extra = extra) - on.exit(do.call(base::options, saved), add = TRUE) - } else if (nzchar(Sys.which("wget")) && nzchar(pat)) { - fmt <- "--header=\"Authorization: token %s\"" - extra <- sprintf(fmt, pat) - saved <- options("download.file.method", "download.file.extra") - options(download.file.method = "wget", download.file.extra = extra) - on.exit(do.call(base::options, saved), add = TRUE) - } - - message("* Downloading renv ", version, " from GitHub ... ", appendLF = FALSE) - - url <- file.path("https://api.github.com/repos/rstudio/renv/tarball", version) - name <- sprintf("renv_%s.tar.gz", version) - destfile <- file.path(tempdir(), name) - - status <- tryCatch( - renv_bootstrap_download_impl(url, destfile), - condition = identity - ) - - if (!identical(status, 0L)) { - message("FAILED") - return(FALSE) - } - - message("OK") - return(destfile) - - } - - renv_bootstrap_install <- function(version, tarball, library) { - - # attempt to install it into project library - message("* Installing renv ", version, " ... ", appendLF = FALSE) - dir.create(library, showWarnings = FALSE, recursive = TRUE) - - # invoke using system2 so we can capture and report output - bin <- R.home("bin") - exe <- if (Sys.info()[["sysname"]] == "Windows") "R.exe" else "R" - r <- file.path(bin, exe) - args <- c("--vanilla", "CMD", "INSTALL", "-l", shQuote(library), shQuote(tarball)) - output <- system2(r, args, stdout = TRUE, stderr = TRUE) - message("Done!") - - # check for successful install - status <- attr(output, "status") - if (is.numeric(status) && !identical(status, 0L)) { - header <- "Error installing renv:" - lines <- paste(rep.int("=", nchar(header)), collapse = "") - text <- c(header, lines, output) - writeLines(text, con = stderr()) - } - - status - - } - - renv_bootstrap_prefix <- function() { - - # construct version prefix - version <- paste(R.version$major, R.version$minor, sep = ".") - prefix <- paste("R", numeric_version(version)[1, 1:2], sep = "-") - - # include SVN revision for development versions of R - # (to avoid sharing platform-specific artefacts with released versions of R) - devel <- - identical(R.version[["status"]], "Under development (unstable)") || - identical(R.version[["nickname"]], "Unsuffered Consequences") - - if (devel) - prefix <- paste(prefix, R.version[["svn rev"]], sep = "-r") - - # build list of path components - components <- c(prefix, R.version$platform) - - # include prefix if provided by user - prefix <- Sys.getenv("RENV_PATHS_PREFIX") - if (nzchar(prefix)) - components <- c(prefix, components) - - # build prefix - paste(components, collapse = "/") - - } - - renv_bootstrap_library_root_name <- function(project) { - - # use project name as-is if requested - asis <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT_ASIS", unset = "FALSE") - if (asis) - return(basename(project)) - - # otherwise, disambiguate based on project's path - id <- substring(renv_bootstrap_hash_text(project), 1L, 8L) - paste(basename(project), id, sep = "-") - - } - - renv_bootstrap_library_root <- function(project) { - - path <- Sys.getenv("RENV_PATHS_LIBRARY", unset = NA) - if (!is.na(path)) - return(path) - - path <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT", unset = NA) - if (!is.na(path)) { - name <- renv_bootstrap_library_root_name(project) - return(file.path(path, name)) - } - - file.path(project, "renv/library") - - } - - renv_bootstrap_validate_version <- function(version) { - - loadedversion <- utils::packageDescription("renv", fields = "Version") - if (version == loadedversion) - return(TRUE) - - # assume four-component versions are from GitHub; three-component - # versions are from CRAN - components <- strsplit(loadedversion, "[.-]")[[1]] - remote <- if (length(components) == 4L) - paste("rstudio/renv", loadedversion, sep = "@") - else - paste("renv", loadedversion, sep = "@") - - fmt <- paste( - "renv %1$s was loaded from project library, but this project is configured to use renv %2$s.", - "Use `renv::record(\"%3$s\")` to record renv %1$s in the lockfile.", - "Use `renv::restore(packages = \"renv\")` to install renv %2$s into the project library.", - sep = "\n" - ) - - msg <- sprintf(fmt, loadedversion, version, remote) - warning(msg, call. = FALSE) - - FALSE - - } - - renv_bootstrap_hash_text <- function(text) { - - hashfile <- tempfile("renv-hash-") - on.exit(unlink(hashfile), add = TRUE) - - writeLines(text, con = hashfile) - tools::md5sum(hashfile) - - } - - renv_bootstrap_load <- function(project, libpath, version) { - - # try to load renv from the project library - if (!requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) - return(FALSE) - - # warn if the version of renv loaded does not match - renv_bootstrap_validate_version(version) - - # load the project - renv::load(project) - - TRUE - - } - - # construct path to library root - root <- renv_bootstrap_library_root(project) - - # construct library prefix for platform - prefix <- renv_bootstrap_prefix() - - # construct full libpath - libpath <- file.path(root, prefix) - - # attempt to load - if (renv_bootstrap_load(project, libpath, version)) - return(TRUE) - - # load failed; inform user we're about to bootstrap - prefix <- paste("# Bootstrapping renv", version) - postfix <- paste(rep.int("-", 77L - nchar(prefix)), collapse = "") - header <- paste(prefix, postfix) - message(header) - - # perform bootstrap - bootstrap(version, libpath) - - # exit early if we're just testing bootstrap - if (!is.na(Sys.getenv("RENV_BOOTSTRAP_INSTALL_ONLY", unset = NA))) - return(TRUE) - - # try again to load - if (requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) { - message("* Successfully installed and loaded renv ", version, ".") - return(renv::load()) - } - - # failed to download or load renv; warn the user - msg <- c( - "Failed to find an renv installation: the project will not be loaded.", - "Use `renv::activate()` to re-initialize the project." - ) - - warning(paste(msg, collapse = "\n"), call. = FALSE) - -}) diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py index 0c5e56382..763fe8e9e 100644 --- a/tests/languages/r_test.py +++ b/tests/languages/r_test.py @@ -1,13 +1,16 @@ from __future__ import annotations import os.path +import shutil import pytest from pre_commit import envcontext from pre_commit.languages import r from pre_commit.prefix import Prefix +from pre_commit.store import _make_local_repo from pre_commit.util import win_exe +from testing.language_helpers import run_language def test_r_parsing_file_no_opts_no_args(tmp_path): @@ -97,3 +100,99 @@ def test_rscript_exec_relative_to_r_home(): def test_path_rscript_exec_no_r_home_set(): with envcontext.envcontext((('R_HOME', envcontext.UNSET),)): assert r._rscript_exec() == 'Rscript' + + +def test_r_hook(tmp_path): + renv_lock = '''\ +{ + "R": { + "Version": "4.0.3", + "Repositories": [ + { + "Name": "CRAN", + "URL": "https://cloud.r-project.org" + } + ] + }, + "Packages": { + "renv": { + "Package": "renv", + "Version": "0.12.5", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "5c0cdb37f063c58cdab3c7e9fbb8bd2c" + }, + "rprojroot": { + "Package": "rprojroot", + "Version": "1.0", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "86704667fe0860e4fec35afdfec137f3" + } + } +} +''' + description = '''\ +Package: gli.clu +Title: What the Package Does (One Line, Title Case) +Type: Package +Version: 0.0.0.9000 +Authors@R: + person(given = "First", + family = "Last", + role = c("aut", "cre"), + email = "first.last@example.com", + comment = c(ORCID = "YOUR-ORCID-ID")) +Description: What the package does (one paragraph). +License: `use_mit_license()`, `use_gpl3_license()` or friends to + pick a license +Encoding: UTF-8 +LazyData: true +Roxygen: list(markdown = TRUE) +RoxygenNote: 7.1.1 +Imports: + rprojroot +''' + hello_world_r = '''\ +stopifnot( + packageVersion('rprojroot') == '1.0', + packageVersion('gli.clu') == '0.0.0.9000' +) +cat("Hello, World, from R!\n") +''' + + tmp_path.joinpath('renv.lock').write_text(renv_lock) + tmp_path.joinpath('DESCRIPTION').write_text(description) + tmp_path.joinpath('hello-world.R').write_text(hello_world_r) + renv_dir = tmp_path.joinpath('renv') + renv_dir.mkdir() + shutil.copy( + os.path.join( + os.path.dirname(__file__), + '../../pre_commit/resources/empty_template_activate.R', + ), + renv_dir.joinpath('activate.R'), + ) + + expected = (0, b'Hello, World, from R!\n') + assert run_language(tmp_path, r, 'Rscript hello-world.R') == expected + + +def test_r_inline(tmp_path): + _make_local_repo(str(tmp_path)) + + cmd = '''\ +Rscript -e ' + stopifnot(packageVersion("rprojroot") == "1.0") + cat(commandArgs(trailingOnly = TRUE), "from R!\n", sep=", ") +' +''' + + ret = run_language( + tmp_path, + r, + cmd, + deps=('rprojroot@1.0',), + args=('hi', 'hello'), + ) + assert ret == (0, b'hi, hello, from R!\n') diff --git a/tests/repository_test.py b/tests/repository_test.py index 0d01f0f65..bcb671261 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -227,54 +227,6 @@ def test_node_hook_with_npm_userconfig_set(tempdir_factory, store, tmpdir): test_run_a_node_hook(tempdir_factory, store) -def test_r_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'r_hooks_repo', - 'hello-world', [os.devnull], - b'Hello, World, from R!\n', - ) - - -def test_r_inline_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'r_hooks_repo', - 'hello-world-inline', ['some-file'], - b'Hi-there, some-file, from R!\n', - ) - - -def test_r_with_additional_dependencies_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'r_hooks_repo', - 'additional-deps', [os.devnull], - b'OK\n', - config_kwargs={ - 'hooks': [{ - 'id': 'additional-deps', - 'additional_dependencies': ['cachem@1.0.4'], - }], - }, - ) - - -def test_r_local_with_additional_dependencies_hook(store): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'local-r', - 'name': 'local-r', - 'entry': 'Rscript -e', - 'language': 'r', - 'args': ['if (packageVersion("R6") == "2.1.3") cat("OK\n")'], - 'additional_dependencies': ['R6@2.1.3'], - }], - } - hook = _get_hook(config, store, 'local-r') - ret, out = _hook_run(hook, (), color=False) - assert ret == 0 - assert _norm_out(out) == b'OK\n' - - def test_run_a_ruby_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'ruby_hooks_repo', From f042540311b9c23ba56fa12b87211fb495219c81 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jan 2023 23:43:31 -0500 Subject: [PATCH 732/967] test lua directly --- .../resources/lua_repo/.pre-commit-hooks.yaml | 4 -- .../resources/lua_repo/bin/hello-world-lua | 3 - .../resources/lua_repo/hello-dev-1.rockspec | 15 ----- testing/util.py | 4 -- tests/languages/lua_test.py | 58 +++++++++++++++++++ tests/repository_test.py | 27 --------- 6 files changed, 58 insertions(+), 53 deletions(-) delete mode 100644 testing/resources/lua_repo/.pre-commit-hooks.yaml delete mode 100755 testing/resources/lua_repo/bin/hello-world-lua delete mode 100644 testing/resources/lua_repo/hello-dev-1.rockspec create mode 100644 tests/languages/lua_test.py diff --git a/testing/resources/lua_repo/.pre-commit-hooks.yaml b/testing/resources/lua_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 767ef972c..000000000 --- a/testing/resources/lua_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,4 +0,0 @@ -- id: hello-world-lua - name: hello world lua - entry: hello-world-lua - language: lua diff --git a/testing/resources/lua_repo/bin/hello-world-lua b/testing/resources/lua_repo/bin/hello-world-lua deleted file mode 100755 index 2a0e00246..000000000 --- a/testing/resources/lua_repo/bin/hello-world-lua +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env lua - -print('hello world') diff --git a/testing/resources/lua_repo/hello-dev-1.rockspec b/testing/resources/lua_repo/hello-dev-1.rockspec deleted file mode 100644 index 82486e08a..000000000 --- a/testing/resources/lua_repo/hello-dev-1.rockspec +++ /dev/null @@ -1,15 +0,0 @@ -package = "hello" -version = "dev-1" - -source = { - url = "git+ssh://git@github.com/pre-commit/pre-commit.git" -} -description = {} -dependencies = {} -build = { - type = "builtin", - modules = {}, - install = { - bin = {"bin/hello-world-lua"} - }, -} diff --git a/testing/util.py b/testing/util.py index a5ae06d05..b6c3804e6 100644 --- a/testing/util.py +++ b/testing/util.py @@ -45,10 +45,6 @@ def cmd_output_mocked_pre_commit_home( os.name == 'nt' or not docker_is_running(), reason="Docker isn't running or can't be accessed", ) -skipif_cant_run_lua = pytest.mark.skipif( - os.name == 'nt', - reason="lua isn't installed or can't be found", -) xfailif_windows = pytest.mark.xfail(os.name == 'nt', reason='windows') diff --git a/tests/languages/lua_test.py b/tests/languages/lua_test.py new file mode 100644 index 000000000..b2767b727 --- /dev/null +++ b/tests/languages/lua_test.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import sys + +import pytest + +from pre_commit.languages import lua +from pre_commit.util import make_executable +from testing.language_helpers import run_language + +pytestmark = pytest.mark.skipif( + sys.platform == 'win32', + reason='lua is not supported on windows', +) + + +def test_lua(tmp_path): # pragma: win32 no cover + rockspec = '''\ +package = "hello" +version = "dev-1" + +source = { + url = "git+ssh://git@github.com/pre-commit/pre-commit.git" +} +description = {} +dependencies = {} +build = { + type = "builtin", + modules = {}, + install = { + bin = {"bin/hello-world-lua"} + }, +} +''' + hello_world_lua = '''\ +#!/usr/bin/env lua +print('hello world') +''' + tmp_path.joinpath('hello-dev-1.rockspec').write_text(rockspec) + bin_dir = tmp_path.joinpath('bin') + bin_dir.mkdir() + bin_file = bin_dir.joinpath('hello-world-lua') + bin_file.write_text(hello_world_lua) + make_executable(bin_file) + + expected = (0, b'hello world\n') + assert run_language(tmp_path, lua, 'hello-world-lua') == expected + + +def test_lua_additional_dependencies(tmp_path): # pragma: win32 no cover + ret, out = run_language( + tmp_path, + lua, + 'luacheck --version', + deps=('luacheck',), + ) + assert ret == 0 + assert out.startswith(b'Luacheck: ') diff --git a/tests/repository_test.py b/tests/repository_test.py index 0d01f0f65..a617da1df 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -33,7 +33,6 @@ from testing.util import cwd from testing.util import get_resource_path from testing.util import skipif_cant_run_docker -from testing.util import skipif_cant_run_lua from testing.util import xfailif_windows @@ -1041,29 +1040,3 @@ def test_non_installable_hook_error_for_additional_dependencies(store, caplog): 'using language `system` which does not install an environment. ' 'Perhaps you meant to use a specific language?' ) - - -@skipif_cant_run_lua # pragma: win32 no cover -def test_lua_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'lua_repo', - 'hello-world-lua', [], b'hello world\n', - ) - - -@skipif_cant_run_lua # pragma: win32 no cover -def test_local_lua_additional_dependencies(store): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'local-lua', - 'name': 'local-lua', - 'entry': 'luacheck --version', - 'language': 'lua', - 'additional_dependencies': ['luacheck'], - }], - } - hook = _get_hook(config, store, 'local-lua') - ret, out = _hook_run(hook, (), color=False) - assert b'Luacheck' in out - assert ret == 0 From 14c38d18fcc608644db077f7c862f9892a981668 Mon Sep 17 00:00:00 2001 From: Jamie Alessio Date: Sat, 21 Jan 2023 11:05:13 -0800 Subject: [PATCH 733/967] Upgrade to ruby-build v20221225 --- pre_commit/resources/ruby-build.tar.gz | Bin 74032 -> 76466 bytes testing/make-archives | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz index 35419f63aebe33b6710851aef70937cd9ef163ba..b6eacf59ba31c87032e76c2d1f39a87bb2b52489 100644 GIT binary patch literal 76466 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

Q#vRC5;yit2Sd`xrHX$e_C?P2bQUVfE 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

@UO6IsN-Ld{vdH%#= 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

FB_o9 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%0Da>5o|PaFgyRW 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 Date: Mon, 23 Jan 2023 19:24:10 -0500 Subject: [PATCH 734/967] introduce install state v2 to replace v1 the v1 state is unnecessary since new repos are created for new additional_dependencies --- pre_commit/constants.py | 2 -- pre_commit/repository.py | 25 ++++++++++++++++++------- tests/repository_test.py | 16 ++++++++++++++++ 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 8fc5e55db..3f03ceed9 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -5,8 +5,6 @@ CONFIG_FILE = '.pre-commit-config.yaml' MANIFEST_FILE = '.pre-commit-hooks.yaml' -# Bump when installation changes in a backwards / forwards incompatible way -INSTALLED_STATE_VERSION = '1' # Bump when modifying `empty_template` LOCAL_REPO_VERSION = '1' diff --git a/pre_commit/repository.py b/pre_commit/repository.py index ac6b84463..616faf54c 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -23,16 +23,20 @@ logger = logging.getLogger('pre_commit') -def _state(additional_deps: Sequence[str]) -> object: - return {'additional_dependencies': sorted(additional_deps)} +def _state_filename_v1(venv: str) -> str: + return os.path.join(venv, '.install_state_v1') + + +def _state_filename_v2(venv: str) -> str: + return os.path.join(venv, '.install_state_v2') -def _state_filename(venv: str) -> str: - return os.path.join(venv, f'.install_state_v{C.INSTALLED_STATE_VERSION}') +def _state(additional_deps: Sequence[str]) -> object: + return {'additional_dependencies': sorted(additional_deps)} def _read_state(venv: str) -> object | None: - filename = _state_filename(venv) + filename = _state_filename_v1(venv) if not os.path.exists(filename): return None else: @@ -51,7 +55,10 @@ def _hook_installed(hook: Hook) -> bool: hook.language_version, ) return ( - _read_state(venv) == _state(hook.additional_dependencies) and + ( + os.path.exists(_state_filename_v2(venv)) or + _read_state(venv) == _state(hook.additional_dependencies) + ) and not lang.health_check(hook.prefix, hook.language_version) ) @@ -87,14 +94,18 @@ def _hook_install(hook: Hook) -> None: f'your environment\n\n' f'more info:\n\n{health_error}', ) + + # TODO: remove v1 state writing, no longer needed after pre-commit 3.0 # Write our state to indicate we're installed - state_filename = _state_filename(venv) + state_filename = _state_filename_v1(venv) staging = f'{state_filename}staging' with open(staging, 'w') as state_file: state_file.write(json.dumps(_state(hook.additional_dependencies))) # Move the file into place atomically to indicate we've installed os.replace(staging, state_filename) + open(_state_filename_v2(venv), 'a+').close() + def _hook( *hook_dicts: dict[str, Any], diff --git a/tests/repository_test.py b/tests/repository_test.py index 8d3034bb9..da8785963 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -23,6 +23,7 @@ from pre_commit.languages import rust from pre_commit.languages.all import languages from pre_commit.prefix import Prefix +from pre_commit.repository import _hook_installed from pre_commit.repository import all_hooks from pre_commit.repository import install_hook_envs from pre_commit.util import cmd_output @@ -562,6 +563,21 @@ def test_additional_dependencies_roll_forward(tempdir_factory, store): assert 'mccabe' not in cmd_output('pip', 'freeze', '-l')[1] +@pytest.mark.parametrize('v', ('v1', 'v2')) +def test_repository_state_compatibility(tempdir_factory, store, v): + path = make_repo(tempdir_factory, 'python_hooks_repo') + + config = make_config_from_repo(path) + hook = _get_hook(config, store, 'foo') + envdir = helpers.environment_dir( + hook.prefix, + python.ENVIRONMENT_DIR, + hook.language_version, + ) + os.remove(os.path.join(envdir, f'.install_state_{v}')) + assert _hook_installed(hook) is True + + def test_additional_ruby_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'ruby_hooks_repo') config = make_config_from_repo(path) From 6b88fe577c44472d234e8d4d8ee89ca36e03ae2a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 23 Jan 2023 20:40:13 -0500 Subject: [PATCH 735/967] v3.0.0 --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd0de5f73..59e0e202b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,37 @@ +3.0.0 - 2023-01-23 +================== + +### Features +- Make `language: golang` bootstrap `go` if not present. + - #2651 PR by @taoufik07. + - #2649 issue by @taoufik07. +- `language: coursier` now supports `additional_dependencies` and `repo: local` + - #2702 PR by @asottile. +- Upgrade `ruby-build` to `20221225`. + - #2718 PR by @jalessio. + +### Fixes +- Improve error message for invalid yaml for `pre-commit autoupdate`. + - #2686 PR by @asottile. + - #2685 issue by @CarstenGrohmann. +- `repo: local` no longer provisions an empty `git` repo. + - #2699 PR by @asottile. + +### Updating +- Drop support for python<3.8 + - #2655 PR by @asottile. +- Drop support for top-level list, use `pre-commit migrate-config` to update. + - #2656 PR by @asottile. +- Drop support for `sha` to specify revision, use `pre-commit migrate-config` + to update. + - #2657 PR by @asottile. +- Remove `pre-commit-validate-config` and `pre-commit-validate-manifest`, use + `pre-commit validate-config` and `pre-commit validate-manifest` instead. + - #2658 PR by @asottile. +- `language: golang` hooks must use `go.mod` to specify dependencies + - #2672 PR by @taoufik07. + + 2.21.0 - 2022-12-25 =================== diff --git a/setup.cfg b/setup.cfg index ca1f7d8bd..929f4c327 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.21.0 +version = 3.0.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 83e05e607e6b8cfde97c05e067d156be09a298a9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 25 Jan 2023 14:03:39 -0500 Subject: [PATCH 736/967] ensure coursier hooks are available offline after install --- pre_commit/languages/coursier.py | 45 ++++++++++++++++---------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index 69c877d32..60757588d 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -28,45 +28,44 @@ def install_environment( helpers.assert_version_default('coursier', version) # Support both possible executable names (either "cs" or "coursier") - executable = find_executable('cs') or find_executable('coursier') - if executable is None: + cs = find_executable('cs') or find_executable('coursier') + if cs is None: raise AssertionError( 'pre-commit requires system-installed "cs" or "coursier" ' 'executables in the application search path', ) envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) - channel = prefix.path('.pre-commit-channel') - if os.path.isdir(channel): - for app_descriptor in os.listdir(channel): - _, app_file = os.path.split(app_descriptor) - app, _ = os.path.splitext(app_file) - helpers.run_setup_cmd( - prefix, - ( - executable, - 'install', + + def _install(*opts: str) -> None: + assert cs is not None + helpers.run_setup_cmd(prefix, (cs, 'fetch', *opts)) + helpers.run_setup_cmd(prefix, (cs, 'install', '--dir', envdir, *opts)) + + with in_env(prefix, version): + channel = prefix.path('.pre-commit-channel') + if os.path.isdir(channel): + for app_descriptor in os.listdir(channel): + _, app_file = os.path.split(app_descriptor) + app, _ = os.path.splitext(app_file) + _install( '--default-channels=false', '--channel', channel, - '--dir', envdir, app, - ), + ) + elif not additional_dependencies: + raise FatalError( + 'expected .pre-commit-channel dir or additional_dependencies', ) - elif not additional_dependencies: - raise FatalError( - 'expected .pre-commit-channel dir or additional_dependencies', - ) - if additional_dependencies: - install_cmd = ( - executable, 'install', '--dir', envdir, *additional_dependencies, - ) - helpers.run_setup_cmd(prefix, install_cmd) + if additional_dependencies: + _install(*additional_dependencies) def get_env_patch(target_dir: str) -> PatchesT: return ( ('PATH', (target_dir, os.pathsep, Var('PATH'))), + ('COURSIER_CACHE', os.path.join(target_dir, '.cs-cache')), ) From dd8e717ed6022209a2b0cecf5c75460eb60e548e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 26 Jan 2023 11:09:17 -0500 Subject: [PATCH 737/967] v3.0.1 --- CHANGELOG.md | 7 +++++++ setup.cfg | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59e0e202b..d55ff7325 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +3.0.1 - 2023-01-26 +================== + +### Fixes +- Ensure coursier hooks are available offline after install. + - #2723 PR by @asottile. + 3.0.0 - 2023-01-23 ================== diff --git a/setup.cfg b/setup.cfg index 929f4c327..1dbace59c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.0.0 +version = 3.0.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From f4bd44996c888f48bc3a37d5ab19514325cb3f01 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Jan 2023 16:44:44 -0500 Subject: [PATCH 738/967] also ignore Gemfile in project this starts failing with ruby 3.2.0 --- pre_commit/languages/ruby.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 4416f7280..b4d4b45af 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -39,6 +39,7 @@ def get_env_patch( ('GEM_HOME', os.path.join(venv, 'gems')), ('GEM_PATH', UNSET), ('BUNDLE_IGNORE_CONFIG', '1'), + ('BUNDLE_GEMFILE', os.devnull), ) if language_version == 'system': patches += ( From 6e8051b9e644505f2755ed576cb4b7220f6db8b4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Jan 2023 16:20:50 -0500 Subject: [PATCH 739/967] speed up ruby tests by picking a prebuilt in 22.04 --- .../ruby_versioned_hooks_repo/.pre-commit-hooks.yaml | 2 +- tests/languages/ruby_test.py | 4 ++-- tests/repository_test.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml index 364d47d8f..c97939ad9 100644 --- a/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml +++ b/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml @@ -2,5 +2,5 @@ name: Ruby Hook entry: ruby_hook language: ruby - language_version: 3.1.0 + language_version: 3.2.0 files: \.rb$ diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 29f3c802e..63a16eb11 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -71,10 +71,10 @@ def test_install_ruby_default(fake_gem_prefix): @xfailif_windows # pragma: win32 no cover def test_install_ruby_with_version(fake_gem_prefix): - ruby.install_environment(fake_gem_prefix, '3.1.0', ()) + ruby.install_environment(fake_gem_prefix, '3.2.0', ()) # Should be able to activate and use rbenv install - with ruby.in_env(fake_gem_prefix, '3.1.0'): + with ruby.in_env(fake_gem_prefix, '3.2.0'): cmd_output('rbenv', 'install', '--help') diff --git a/tests/repository_test.py b/tests/repository_test.py index da8785963..ff2d7c323 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -247,7 +247,7 @@ def test_run_versioned_ruby_hook(tempdir_factory, store): tempdir_factory, store, 'ruby_versioned_hooks_repo', 'ruby_hook', [os.devnull], - b'3.1.0\nHello world from a ruby hook\n', + b'3.2.0\nHello world from a ruby hook\n', ) @@ -269,7 +269,7 @@ def test_run_ruby_hook_with_disable_shared_gems( tempdir_factory, store, 'ruby_versioned_hooks_repo', 'ruby_hook', [os.devnull], - b'3.1.0\nHello world from a ruby hook\n', + b'3.2.0\nHello world from a ruby hook\n', ) From 420902f67cbd2117e93f797191eaa9dab4be6904 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 29 Jan 2023 17:27:42 -0500 Subject: [PATCH 740/967] fix r local hooks `language: r` acts more like `language: script` so we have to *not* append the prefix when run with `repo: local` --- pre_commit/commands/run.py | 1 + pre_commit/languages/all.py | 1 + pre_commit/languages/docker.py | 1 + pre_commit/languages/docker_image.py | 1 + pre_commit/languages/fail.py | 1 + pre_commit/languages/helpers.py | 1 + pre_commit/languages/pygrep.py | 1 + pre_commit/languages/r.py | 17 ++++++++++++---- pre_commit/languages/script.py | 1 + testing/language_helpers.py | 2 ++ tests/languages/r_test.py | 29 ++++++++++++++++++++++++++-- tests/repository_test.py | 1 + 12 files changed, 51 insertions(+), 6 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 85fa59aa1..e44e70364 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -195,6 +195,7 @@ def _run_single_hook( hook.entry, hook.args, filenames, + is_local=hook.src == 'local', require_serial=hook.require_serial, color=use_color, ) diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index c7aab65e7..d952ae1ab 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -66,6 +66,7 @@ def run_hook( args: Sequence[str], file_args: Sequence[str], *, + is_local: bool, require_serial: bool, color: bool, ) -> tuple[int, bytes]: diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 18234567b..e80c95978 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -127,6 +127,7 @@ def run_hook( args: Sequence[str], file_args: Sequence[str], *, + is_local: bool, require_serial: bool, color: bool, ) -> tuple[int, bytes]: # pragma: win32 no cover diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index 230983823..8e5f2c04c 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -19,6 +19,7 @@ def run_hook( args: Sequence[str], file_args: Sequence[str], *, + is_local: bool, require_serial: bool, color: bool, ) -> tuple[int, bytes]: # pragma: win32 no cover diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index 13b2bc12c..33df067e4 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -18,6 +18,7 @@ def run_hook( args: Sequence[str], file_args: Sequence[str], *, + is_local: bool, require_serial: bool, color: bool, ) -> tuple[int, bytes]: diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 074f98e9f..d1be409c8 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -146,6 +146,7 @@ def basic_run_hook( args: Sequence[str], file_args: Sequence[str], *, + is_local: bool, require_serial: bool, color: bool, ) -> tuple[int, bytes]: diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 93e2a65bd..f0eb9a959 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -93,6 +93,7 @@ def run_hook( args: Sequence[str], file_args: Sequence[str], *, + is_local: bool, require_serial: bool, color: bool, ) -> tuple[int, bytes]: diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index dc3986057..e2383658a 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -35,8 +35,13 @@ def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: yield -def _prefix_if_file_entry(entry: list[str], prefix: Prefix) -> Sequence[str]: - if entry[1] == '-e': +def _prefix_if_file_entry( + entry: list[str], + prefix: Prefix, + *, + is_local: bool, +) -> Sequence[str]: + if entry[1] == '-e' or is_local: return entry[1:] else: return (prefix.path(entry[1]),) @@ -73,11 +78,14 @@ def _cmd_from_hook( prefix: Prefix, entry: str, args: Sequence[str], + *, + is_local: bool, ) -> tuple[str, ...]: cmd = shlex.split(entry) _entry_validate(cmd) - return (cmd[0], *RSCRIPT_OPTS, *_prefix_if_file_entry(cmd, prefix), *args) + cmd_part = _prefix_if_file_entry(cmd, prefix, is_local=is_local) + return (cmd[0], *RSCRIPT_OPTS, *cmd_part, *args) def install_environment( @@ -153,10 +161,11 @@ def run_hook( args: Sequence[str], file_args: Sequence[str], *, + is_local: bool, require_serial: bool, color: bool, ) -> tuple[int, bytes]: - cmd = _cmd_from_hook(prefix, entry, args) + cmd = _cmd_from_hook(prefix, entry, args, is_local=is_local) return helpers.run_xargs( cmd, file_args, diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 41fffdf07..08325f469 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -18,6 +18,7 @@ def run_hook( args: Sequence[str], file_args: Sequence[str], *, + is_local: bool, require_serial: bool, color: bool, ) -> tuple[int, bytes]: diff --git a/testing/language_helpers.py b/testing/language_helpers.py index 02e47a002..f9ae0b1da 100644 --- a/testing/language_helpers.py +++ b/testing/language_helpers.py @@ -16,6 +16,7 @@ def run_language( file_args: Sequence[str] = (), version: str = C.DEFAULT, deps: Sequence[str] = (), + is_local: bool = False, ) -> tuple[int, bytes]: prefix = Prefix(str(path)) @@ -26,6 +27,7 @@ def run_language( exe, args, file_args, + is_local=is_local, require_serial=True, color=False, ) diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py index 763fe8e9e..02c559cb4 100644 --- a/tests/languages/r_test.py +++ b/tests/languages/r_test.py @@ -14,7 +14,12 @@ def test_r_parsing_file_no_opts_no_args(tmp_path): - cmd = r._cmd_from_hook(Prefix(str(tmp_path)), 'Rscript some-script.R', ()) + cmd = r._cmd_from_hook( + Prefix(str(tmp_path)), + 'Rscript some-script.R', + (), + is_local=False, + ) assert cmd == ( 'Rscript', '--no-save', '--no-restore', '--no-site-file', '--no-environ', @@ -38,6 +43,7 @@ def test_r_parsing_file_no_opts_args(tmp_path): Prefix(str(tmp_path)), 'Rscript some-script.R', ('--no-cache',), + is_local=False, ) assert cmd == ( 'Rscript', @@ -48,7 +54,12 @@ def test_r_parsing_file_no_opts_args(tmp_path): def test_r_parsing_expr_no_opts_no_args1(tmp_path): - cmd = r._cmd_from_hook(Prefix(str(tmp_path)), "Rscript -e '1+1'", ()) + cmd = r._cmd_from_hook( + Prefix(str(tmp_path)), + "Rscript -e '1+1'", + (), + is_local=False, + ) assert cmd == ( 'Rscript', '--no-save', '--no-restore', '--no-site-file', '--no-environ', @@ -56,6 +67,20 @@ def test_r_parsing_expr_no_opts_no_args1(tmp_path): ) +def test_r_parsing_local_hook_path_is_not_expanded(tmp_path): + cmd = r._cmd_from_hook( + Prefix(str(tmp_path)), + 'Rscript path/to/thing.R', + (), + is_local=True, + ) + assert cmd == ( + 'Rscript', + '--no-save', '--no-restore', '--no-site-file', '--no-environ', + 'path/to/thing.R', + ) + + def test_r_parsing_expr_no_opts_no_args2(): with pytest.raises(ValueError) as excinfo: r._entry_validate(['Rscript', '-e', '1+1', '-e', 'letters']) diff --git a/tests/repository_test.py b/tests/repository_test.py index ff2d7c323..85cf45812 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -48,6 +48,7 @@ def _hook_run(hook, filenames, color): hook.entry, hook.args, filenames, + is_local=hook.src == 'local', require_serial=hook.require_serial, color=color, ) From 2adca78c6feb99d0e9b14158fa38e599ec7e84a6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 29 Jan 2023 18:27:10 -0500 Subject: [PATCH 741/967] test rust directly --- testing/language_helpers.py | 4 +- .../rust_hooks_repo/.pre-commit-hooks.yaml | 5 - testing/resources/rust_hooks_repo/Cargo.lock | 3 - testing/resources/rust_hooks_repo/Cargo.toml | 3 - testing/resources/rust_hooks_repo/src/main.rs | 3 - tests/languages/rust_test.py | 101 ++++++++++-------- tests/repository_test.py | 66 ------------ 7 files changed, 59 insertions(+), 126 deletions(-) delete mode 100644 testing/resources/rust_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/rust_hooks_repo/Cargo.lock delete mode 100644 testing/resources/rust_hooks_repo/Cargo.toml delete mode 100644 testing/resources/rust_hooks_repo/src/main.rs diff --git a/testing/language_helpers.py b/testing/language_helpers.py index 02e47a002..45fefbabd 100644 --- a/testing/language_helpers.py +++ b/testing/language_helpers.py @@ -3,7 +3,6 @@ import os from typing import Sequence -import pre_commit.constants as C from pre_commit.languages.all import Language from pre_commit.prefix import Prefix @@ -14,10 +13,11 @@ def run_language( exe: str, args: Sequence[str] = (), file_args: Sequence[str] = (), - version: str = C.DEFAULT, + version: str | None = None, deps: Sequence[str] = (), ) -> tuple[int, bytes]: prefix = Prefix(str(path)) + version = version or language.get_default_version() language.install_environment(prefix, version, deps) with language.in_env(prefix, version): diff --git a/testing/resources/rust_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/rust_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index df1269ff8..000000000 --- a/testing/resources/rust_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: rust-hook - name: rust example hook - entry: rust-hello-world - language: rust - files: '' diff --git a/testing/resources/rust_hooks_repo/Cargo.lock b/testing/resources/rust_hooks_repo/Cargo.lock deleted file mode 100644 index 36fbfda2b..000000000 --- a/testing/resources/rust_hooks_repo/Cargo.lock +++ /dev/null @@ -1,3 +0,0 @@ -[[package]] -name = "rust-hello-world" -version = "0.1.0" diff --git a/testing/resources/rust_hooks_repo/Cargo.toml b/testing/resources/rust_hooks_repo/Cargo.toml deleted file mode 100644 index cd83b4358..000000000 --- a/testing/resources/rust_hooks_repo/Cargo.toml +++ /dev/null @@ -1,3 +0,0 @@ -[package] -name = "rust-hello-world" -version = "0.1.0" diff --git a/testing/resources/rust_hooks_repo/src/main.rs b/testing/resources/rust_hooks_repo/src/main.rs deleted file mode 100644 index ad379d6ea..000000000 --- a/testing/resources/rust_hooks_repo/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("hello world"); -} diff --git a/tests/languages/rust_test.py b/tests/languages/rust_test.py index b8167a9e3..5c17f5b69 100644 --- a/tests/languages/rust_test.py +++ b/tests/languages/rust_test.py @@ -1,6 +1,5 @@ from __future__ import annotations -from typing import Mapping from unittest import mock import pytest @@ -8,8 +7,8 @@ import pre_commit.constants as C from pre_commit import parse_shebang from pre_commit.languages import rust -from pre_commit.prefix import Prefix -from pre_commit.util import cmd_output +from pre_commit.store import _make_local_repo +from testing.language_helpers import run_language ACTUAL_GET_DEFAULT_VERSION = rust.get_default_version.__wrapped__ @@ -30,64 +29,78 @@ def test_uses_default_when_rust_is_not_available(cmd_output_b_mck): assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT -@pytest.mark.parametrize('language_version', (C.DEFAULT, '1.56.0')) -def test_installs_with_bootstrapped_rustup(tmpdir, language_version): - tmpdir.join('src', 'main.rs').ensure().write( +def _make_hello_world(tmp_path): + src_dir = tmp_path.joinpath('src') + src_dir.mkdir() + src_dir.joinpath('main.rs').write_text( 'fn main() {\n' ' println!("Hello, world!");\n' '}\n', ) - tmpdir.join('Cargo.toml').ensure().write( + tmp_path.joinpath('Cargo.toml').write_text( '[package]\n' 'name = "hello_world"\n' 'version = "0.1.0"\n' 'edition = "2021"\n', ) - prefix = Prefix(str(tmpdir)) - find_executable_exes = [] - original_find_executable = parse_shebang.find_executable +def test_installs_rust_missing_rustup(tmp_path): + _make_hello_world(tmp_path) - def mocked_find_executable( - exe: str, *, env: Mapping[str, str] | None = None, - ) -> str | None: - """ - Return `None` the first time `find_executable` is called to ensure - that the bootstrapping code is executed, then just let the function - work as normal. + # pretend like `rustup` doesn't exist so it gets bootstrapped + calls = [] + orig = parse_shebang.find_executable - Also log the arguments to ensure that everything works as expected. - """ - find_executable_exes.append(exe) - if len(find_executable_exes) == 1: + def mck(exe, env=None): + calls.append(exe) + if len(calls) == 1: + assert exe == 'rustup' return None - return original_find_executable(exe, env=env) + return orig(exe, env=env) - with mock.patch.object(parse_shebang, 'find_executable') as find_exe_mck: - find_exe_mck.side_effect = mocked_find_executable - rust.install_environment(prefix, language_version, ()) - assert find_executable_exes == ['rustup', 'rustup', 'cargo'] + with mock.patch.object(parse_shebang, 'find_executable', side_effect=mck): + ret = run_language(tmp_path, rust, 'hello_world', version='1.56.0') + assert calls == ['rustup', 'rustup', 'cargo', 'hello_world'] + assert ret == (0, b'Hello, world!\n') - with rust.in_env(prefix, language_version): - assert cmd_output('hello_world')[1] == 'Hello, world!\n' +@pytest.mark.parametrize('version', (C.DEFAULT, '1.56.0')) +def test_language_version_with_rustup(tmp_path, version): + assert parse_shebang.find_executable('rustup') is not None -def test_installs_with_existing_rustup(tmpdir): - tmpdir.join('src', 'main.rs').ensure().write( - 'fn main() {\n' - ' println!("Hello, world!");\n' - '}\n', - ) - tmpdir.join('Cargo.toml').ensure().write( - '[package]\n' - 'name = "hello_world"\n' - 'version = "0.1.0"\n' - 'edition = "2021"\n', + _make_hello_world(tmp_path) + + ret = run_language(tmp_path, rust, 'hello_world', version=version) + assert ret == (0, b'Hello, world!\n') + + +@pytest.mark.parametrize('dep', ('cli:shellharden:4.2.0', 'cli:shellharden')) +def test_rust_cli_additional_dependencies(tmp_path, dep): + _make_local_repo(str(tmp_path)) + + t_sh = tmp_path.joinpath('t.sh') + t_sh.write_text('echo $hi\n') + + assert rust.get_default_version() == 'system' + ret = run_language( + tmp_path, + rust, + 'shellharden --transform', + deps=(dep,), + args=(str(t_sh),), ) - prefix = Prefix(str(tmpdir)) + assert ret == (0, b'echo "$hi"\n') - assert parse_shebang.find_executable('rustup') is not None - rust.install_environment(prefix, '1.56.0', ()) - with rust.in_env(prefix, '1.56.0'): - assert cmd_output('hello_world')[1] == 'Hello, world!\n' + +def test_run_lib_additional_dependencies(tmp_path): + _make_hello_world(tmp_path) + + deps = ('shellharden:4.2.0', 'git-version') + ret = run_language(tmp_path, rust, 'hello_world', deps=deps) + assert ret == (0, b'Hello, world!\n') + + bin_dir = tmp_path.joinpath('rustenv-system', 'bin') + assert bin_dir.is_dir() + assert not bin_dir.joinpath('shellharden').exists() + assert not bin_dir.joinpath('shellharden.exe').exists() diff --git a/tests/repository_test.py b/tests/repository_test.py index ff2d7c323..aea7ffbc7 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -20,7 +20,6 @@ from pre_commit.languages import node from pre_commit.languages import python from pre_commit.languages import ruby -from pre_commit.languages import rust from pre_commit.languages.all import languages from pre_commit.prefix import Prefix from pre_commit.repository import _hook_installed @@ -366,54 +365,6 @@ def test_golang_with_recursive_submodule(tmpdir, tempdir_factory, store): assert _norm_out(out) == b'hello hello world\n' -def test_rust_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'rust_hooks_repo', - 'rust-hook', [], b'hello world\n', - ) - - -@pytest.mark.parametrize('dep', ('cli:shellharden:3.1.0', 'cli:shellharden')) -def test_additional_rust_cli_dependencies_installed( - tempdir_factory, store, dep, -): - path = make_repo(tempdir_factory, 'rust_hooks_repo') - config = make_config_from_repo(path) - # A small rust package with no dependencies. - config['hooks'][0]['additional_dependencies'] = [dep] - hook = _get_hook(config, store, 'rust-hook') - envdir = helpers.environment_dir( - hook.prefix, - rust.ENVIRONMENT_DIR, - 'system', - ) - binaries = os.listdir(os.path.join(envdir, 'bin')) - # normalize for windows - binaries = [os.path.splitext(binary)[0] for binary in binaries] - assert 'shellharden' in binaries - - -def test_additional_rust_lib_dependencies_installed( - tempdir_factory, store, -): - path = make_repo(tempdir_factory, 'rust_hooks_repo') - config = make_config_from_repo(path) - # A small rust package with no dependencies. - deps = ['shellharden:3.1.0', 'git-version'] - config['hooks'][0]['additional_dependencies'] = deps - hook = _get_hook(config, store, 'rust-hook') - envdir = helpers.environment_dir( - hook.prefix, - rust.ENVIRONMENT_DIR, - 'system', - ) - binaries = os.listdir(os.path.join(envdir, 'bin')) - # normalize for windows - binaries = [os.path.splitext(binary)[0] for binary in binaries] - assert 'rust-hello-world' in binaries - assert 'shellharden' not in binaries - - def test_missing_executable(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'not_found_exe', @@ -636,23 +587,6 @@ def test_local_golang_additional_dependencies(store): assert _norm_out(out) == b'Hello, Go examples!\n' -def test_local_rust_additional_dependencies(store): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'hello', - 'name': 'hello', - 'entry': 'hello', - 'language': 'rust', - 'additional_dependencies': ['cli:hello-cli:0.2.2'], - }], - } - hook = _get_hook(config, store, 'hello') - ret, out = _hook_run(hook, (), color=False) - assert ret == 0 - assert _norm_out(out) == b'Hello World!\n' - - def test_fail_hooks(store): config = { 'repo': 'local', From 6abb05a60c4087a10c6ce196cd3a8bce065fa6f1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 29 Jan 2023 18:36:45 -0500 Subject: [PATCH 742/967] v3.0.2 --- CHANGELOG.md | 10 ++++++++++ setup.cfg | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d55ff7325..c0657e630 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +3.0.2 - 2023-01-29 +================== + +### Fixes +- Prevent local `Gemfile` from interfering with hook execution. + - #2727 PR by @asottile. +- Fix `language: r`, `repo: local` hooks + - pre-commit-ci/issues#107 by @lorenzwalthert. + - #2728 PR by @asottile. + 3.0.1 - 2023-01-26 ================== diff --git a/setup.cfg b/setup.cfg index 1dbace59c..37511c09e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.0.1 +version = 3.0.2 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 5b50acbd2c3f52f0e8dee3f11e08905430c4aef7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Jan 2023 21:36:13 -0500 Subject: [PATCH 743/967] test ruby directly --- testing/resources/ruby_hooks_repo/.gitignore | 1 - .../ruby_hooks_repo/.pre-commit-hooks.yaml | 5 - .../resources/ruby_hooks_repo/bin/ruby_hook | 3 - .../resources/ruby_hooks_repo/lib/.gitignore | 0 .../ruby_hooks_repo/ruby_hook.gemspec | 9 -- .../ruby_versioned_hooks_repo/.gitignore | 1 - .../.pre-commit-hooks.yaml | 6 - .../ruby_versioned_hooks_repo/bin/ruby_hook | 4 - .../ruby_versioned_hooks_repo/lib/.gitignore | 0 .../ruby_hook.gemspec | 9 -- tests/languages/ruby_test.py | 125 ++++++++++++------ tests/repository_test.py | 58 -------- 12 files changed, 87 insertions(+), 134 deletions(-) delete mode 100644 testing/resources/ruby_hooks_repo/.gitignore delete mode 100644 testing/resources/ruby_hooks_repo/.pre-commit-hooks.yaml delete mode 100755 testing/resources/ruby_hooks_repo/bin/ruby_hook delete mode 100644 testing/resources/ruby_hooks_repo/lib/.gitignore delete mode 100644 testing/resources/ruby_hooks_repo/ruby_hook.gemspec delete mode 100644 testing/resources/ruby_versioned_hooks_repo/.gitignore delete mode 100644 testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml delete mode 100755 testing/resources/ruby_versioned_hooks_repo/bin/ruby_hook delete mode 100644 testing/resources/ruby_versioned_hooks_repo/lib/.gitignore delete mode 100644 testing/resources/ruby_versioned_hooks_repo/ruby_hook.gemspec diff --git a/testing/resources/ruby_hooks_repo/.gitignore b/testing/resources/ruby_hooks_repo/.gitignore deleted file mode 100644 index c111b3313..000000000 --- a/testing/resources/ruby_hooks_repo/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.gem diff --git a/testing/resources/ruby_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/ruby_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index aa15872fb..000000000 --- a/testing/resources/ruby_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: ruby_hook - name: Ruby Hook - entry: ruby_hook - language: ruby - files: \.rb$ diff --git a/testing/resources/ruby_hooks_repo/bin/ruby_hook b/testing/resources/ruby_hooks_repo/bin/ruby_hook deleted file mode 100755 index 5a7e5ed25..000000000 --- a/testing/resources/ruby_hooks_repo/bin/ruby_hook +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env ruby - -puts 'Hello world from a ruby hook' diff --git a/testing/resources/ruby_hooks_repo/lib/.gitignore b/testing/resources/ruby_hooks_repo/lib/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/testing/resources/ruby_hooks_repo/ruby_hook.gemspec b/testing/resources/ruby_hooks_repo/ruby_hook.gemspec deleted file mode 100644 index 75f4e8f7d..000000000 --- a/testing/resources/ruby_hooks_repo/ruby_hook.gemspec +++ /dev/null @@ -1,9 +0,0 @@ -Gem::Specification.new do |s| - s.name = 'ruby_hook' - s.version = '0.1.0' - s.authors = ['Anthony Sottile'] - s.summary = 'A ruby hook!' - s.description = 'A ruby hook!' - s.files = ['bin/ruby_hook'] - s.executables = ['ruby_hook'] -end diff --git a/testing/resources/ruby_versioned_hooks_repo/.gitignore b/testing/resources/ruby_versioned_hooks_repo/.gitignore deleted file mode 100644 index c111b3313..000000000 --- a/testing/resources/ruby_versioned_hooks_repo/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.gem diff --git a/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index c97939ad9..000000000 --- a/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,6 +0,0 @@ -- id: ruby_hook - name: Ruby Hook - entry: ruby_hook - language: ruby - language_version: 3.2.0 - files: \.rb$ diff --git a/testing/resources/ruby_versioned_hooks_repo/bin/ruby_hook b/testing/resources/ruby_versioned_hooks_repo/bin/ruby_hook deleted file mode 100755 index 2406f04cf..000000000 --- a/testing/resources/ruby_versioned_hooks_repo/bin/ruby_hook +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env ruby - -puts RUBY_VERSION -puts 'Hello world from a ruby hook' diff --git a/testing/resources/ruby_versioned_hooks_repo/lib/.gitignore b/testing/resources/ruby_versioned_hooks_repo/lib/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/testing/resources/ruby_versioned_hooks_repo/ruby_hook.gemspec b/testing/resources/ruby_versioned_hooks_repo/ruby_hook.gemspec deleted file mode 100644 index 75f4e8f7d..000000000 --- a/testing/resources/ruby_versioned_hooks_repo/ruby_hook.gemspec +++ /dev/null @@ -1,9 +0,0 @@ -Gem::Specification.new do |s| - s.name = 'ruby_hook' - s.version = '0.1.0' - s.authors = ['Anthony Sottile'] - s.summary = 'A ruby hook!' - s.description = 'A ruby hook!' - s.files = ['bin/ruby_hook'] - s.executables = ['ruby_hook'] -end diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 63a16eb11..b312c7fda 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -1,6 +1,5 @@ from __future__ import annotations -import os.path import tarfile from unittest import mock @@ -8,10 +7,12 @@ import pre_commit.constants as C from pre_commit import parse_shebang +from pre_commit.envcontext import envcontext from pre_commit.languages import ruby -from pre_commit.prefix import Prefix -from pre_commit.util import cmd_output +from pre_commit.store import _make_local_repo from pre_commit.util import resource_bytesio +from testing.language_helpers import run_language +from testing.util import cwd from testing.util import xfailif_windows @@ -34,56 +35,104 @@ def test_uses_system_if_both_gem_and_ruby_are_available(find_exe_mck): assert ACTUAL_GET_DEFAULT_VERSION() == 'system' -@pytest.fixture -def fake_gem_prefix(tmpdir): +@pytest.mark.parametrize( + 'filename', + ('rbenv.tar.gz', 'ruby-build.tar.gz', 'ruby-download.tar.gz'), +) +def test_archive_root_stat(filename): + with resource_bytesio(filename) as f: + with tarfile.open(fileobj=f) as tarf: + root, _, _ = filename.partition('.') + assert oct(tarf.getmember(root).mode) == '0o755' + + +def _setup_hello_world(tmp_path): + bin_dir = tmp_path.joinpath('bin') + bin_dir.mkdir() + bin_dir.joinpath('ruby_hook').write_text( + '#!/usr/bin/env ruby\n' + "puts 'Hello world from a ruby hook'\n", + ) gemspec = '''\ Gem::Specification.new do |s| - s.name = 'pre_commit_placeholder_package' - s.version = '0.0.0' - s.summary = 'placeholder gem for pre-commit hooks' + s.name = 'ruby_hook' + s.version = '0.1.0' s.authors = ['Anthony Sottile'] + s.summary = 'A ruby hook!' + s.description = 'A ruby hook!' + s.files = ['bin/ruby_hook'] + s.executables = ['ruby_hook'] end ''' - tmpdir.join('placeholder_gem.gemspec').write(gemspec) - yield Prefix(tmpdir) + tmp_path.joinpath('ruby_hook.gemspec').write_text(gemspec) -@xfailif_windows # pragma: win32 no cover -def test_install_ruby_system(fake_gem_prefix): - ruby.install_environment(fake_gem_prefix, 'system', ()) +def test_ruby_hook_system(tmp_path): + assert ruby.get_default_version() == 'system' + + _setup_hello_world(tmp_path) + + ret = run_language(tmp_path, ruby, 'ruby_hook') + assert ret == (0, b'Hello world from a ruby hook\n') - # Should be able to activate and use rbenv install - with ruby.in_env(fake_gem_prefix, 'system'): - _, out, _ = cmd_output('gem', 'list') - assert 'pre_commit_placeholder_package' in out + +def test_ruby_with_user_install_set(tmp_path): + gemrc = tmp_path.joinpath('gemrc') + gemrc.write_text('gem: --user-install\n') + + with envcontext((('GEMRC', str(gemrc)),)): + test_ruby_hook_system(tmp_path) + + +def test_ruby_additional_deps(tmp_path): + _make_local_repo(tmp_path) + + ret = run_language( + tmp_path, + ruby, + 'ruby -e', + args=('require "tins"',), + deps=('tins',), + ) + assert ret == (0, b'') @xfailif_windows # pragma: win32 no cover -def test_install_ruby_default(fake_gem_prefix): - ruby.install_environment(fake_gem_prefix, C.DEFAULT, ()) - # Should have created rbenv directory - assert os.path.exists(fake_gem_prefix.path('rbenv-default')) +def test_ruby_hook_default(tmp_path): + _setup_hello_world(tmp_path) - # Should be able to activate using our script and access rbenv - with ruby.in_env(fake_gem_prefix, 'default'): - cmd_output('rbenv', '--help') + out, ret = run_language(tmp_path, ruby, 'rbenv --help', version='default') + assert out == 0 + assert ret.startswith(b'Usage: rbenv ') @xfailif_windows # pragma: win32 no cover -def test_install_ruby_with_version(fake_gem_prefix): - ruby.install_environment(fake_gem_prefix, '3.2.0', ()) +def test_ruby_hook_language_version(tmp_path): + _setup_hello_world(tmp_path) + tmp_path.joinpath('bin', 'ruby_hook').write_text( + '#!/usr/bin/env ruby\n' + 'puts RUBY_VERSION\n' + "puts 'Hello world from a ruby hook'\n", + ) - # Should be able to activate and use rbenv install - with ruby.in_env(fake_gem_prefix, '3.2.0'): - cmd_output('rbenv', 'install', '--help') + ret = run_language(tmp_path, ruby, 'ruby_hook', version='3.2.0') + assert ret == (0, b'3.2.0\nHello world from a ruby hook\n') -@pytest.mark.parametrize( - 'filename', - ('rbenv.tar.gz', 'ruby-build.tar.gz', 'ruby-download.tar.gz'), -) -def test_archive_root_stat(filename): - with resource_bytesio(filename) as f: - with tarfile.open(fileobj=f) as tarf: - root, _, _ = filename.partition('.') - assert oct(tarf.getmember(root).mode) == '0o755' +@xfailif_windows # pragma: win32 no cover +def test_ruby_with_bundle_disable_shared_gems(tmp_path): + workdir = tmp_path.joinpath('workdir') + workdir.mkdir() + # this Gemfile is missing `source` + workdir.joinpath('Gemfile').write_text('gem "lol_hai"\n') + # this bundle config causes things to be written elsewhere + bundle = workdir.joinpath('.bundle') + bundle.mkdir() + bundle.joinpath('config').write_text( + 'BUNDLE_DISABLE_SHARED_GEMS: true\n' + 'BUNDLE_PATH: vendor/gem\n', + ) + + with cwd(workdir): + # `3.2.0` has new enough `gem` requiring `source` and reading `.bundle` + test_ruby_hook_language_version(tmp_path) diff --git a/tests/repository_test.py b/tests/repository_test.py index 6565e1068..2cd4c0fa5 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -19,7 +19,6 @@ from pre_commit.languages import helpers from pre_commit.languages import node from pre_commit.languages import python -from pre_commit.languages import ruby from pre_commit.languages.all import languages from pre_commit.prefix import Prefix from pre_commit.repository import _hook_installed @@ -33,7 +32,6 @@ from testing.util import cwd from testing.util import get_resource_path from testing.util import skipif_cant_run_docker -from testing.util import xfailif_windows def _norm_out(b): @@ -227,52 +225,6 @@ def test_node_hook_with_npm_userconfig_set(tempdir_factory, store, tmpdir): test_run_a_node_hook(tempdir_factory, store) -def test_run_a_ruby_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'ruby_hooks_repo', - 'ruby_hook', [os.devnull], b'Hello world from a ruby hook\n', - ) - - -def test_run_a_ruby_hook_with_user_install_set(tempdir_factory, store, tmpdir): - gemrc = tmpdir.join('gemrc') - gemrc.write('gem: --user-install\n') - with envcontext((('GEMRC', str(gemrc)),)): - test_run_a_ruby_hook(tempdir_factory, store) - - -@xfailif_windows # pragma: win32 no cover -def test_run_versioned_ruby_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'ruby_versioned_hooks_repo', - 'ruby_hook', - [os.devnull], - b'3.2.0\nHello world from a ruby hook\n', - ) - - -@xfailif_windows # pragma: win32 no cover -def test_run_ruby_hook_with_disable_shared_gems( - tempdir_factory, - store, - tmpdir, -): - """Make sure a Gemfile in the project doesn't interfere.""" - tmpdir.join('Gemfile').write('gem "lol_hai"') - tmpdir.join('.bundle').mkdir() - tmpdir.join('.bundle', 'config').write( - 'BUNDLE_DISABLE_SHARED_GEMS: true\n' - 'BUNDLE_PATH: vendor/gem\n', - ) - with cwd(tmpdir.strpath): - _test_hook_repo( - tempdir_factory, store, 'ruby_versioned_hooks_repo', - 'ruby_hook', - [os.devnull], - b'3.2.0\nHello world from a ruby hook\n', - ) - - def test_system_hook_with_spaces(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'system_hook_with_spaces_repo', @@ -530,16 +482,6 @@ def test_repository_state_compatibility(tempdir_factory, store, v): assert _hook_installed(hook) is True -def test_additional_ruby_dependencies_installed(tempdir_factory, store): - path = make_repo(tempdir_factory, 'ruby_hooks_repo') - config = make_config_from_repo(path) - config['hooks'][0]['additional_dependencies'] = ['tins'] - hook = _get_hook(config, store, 'ruby_hook') - with ruby.in_env(hook.prefix, hook.language_version): - output = cmd_output('gem', 'list', '--local')[1] - assert 'tins' in output - - def test_additional_node_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'node_hooks_repo') config = make_config_from_repo(path) From f54386203eebe320175638b3c89dd71fdc2e8674 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Jan 2023 23:04:25 -0500 Subject: [PATCH 744/967] upgrade asottile/workflows to get fast-checkout --- .github/actions/pre-test/action.yml | 2 +- .github/workflows/main.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/pre-test/action.yml b/.github/actions/pre-test/action.yml index 608c0cd11..42bbf00b5 100644 --- a/.github/actions/pre-test/action.yml +++ b/.github/actions/pre-test/action.yml @@ -36,5 +36,5 @@ runs: testing/get-coursier.sh testing/get-dart.sh testing/get-swift.sh - - uses: asottile/workflows/.github/actions/latest-git@v1.2.0 + - uses: asottile/workflows/.github/actions/latest-git@v1.4.0 if: inputs.env == 'py38' && runner.os == 'Linux' diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c78d1051c..f281dcf27 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,12 +12,12 @@ concurrency: jobs: main-windows: - uses: asottile/workflows/.github/workflows/tox.yml@v1.2.0 + uses: asottile/workflows/.github/workflows/tox.yml@v1.4.0 with: env: '["py38"]' os: windows-latest main-linux: - uses: asottile/workflows/.github/workflows/tox.yml@v1.2.0 + uses: asottile/workflows/.github/workflows/tox.yml@v1.4.0 with: env: '["py38", "py39", "py310"]' os: ubuntu-latest From 2530913fad5c648d2614daf5c1a5583fb609fbd8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 31 Jan 2023 20:40:19 -0500 Subject: [PATCH 745/967] ensure languages are healthy after creation --- testing/language_helpers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/testing/language_helpers.py b/testing/language_helpers.py index b20803bce..b9c538403 100644 --- a/testing/language_helpers.py +++ b/testing/language_helpers.py @@ -21,6 +21,8 @@ def run_language( version = version or language.get_default_version() language.install_environment(prefix, version, deps) + health_error = language.health_check(prefix, version) + assert health_error is None, health_error with language.in_env(prefix, version): ret, out = language.run_hook( prefix, From 909dd0e8a1984300b37611a79cf33ad3dd92aa98 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 31 Jan 2023 19:37:37 -0500 Subject: [PATCH 746/967] test node directly --- .../node_hooks_repo/.pre-commit-hooks.yaml | 5 --- testing/resources/node_hooks_repo/bin/main.js | 3 -- .../resources/node_hooks_repo/package.json | 5 --- .../.pre-commit-hooks.yaml | 6 --- .../node_versioned_hooks_repo/bin/main.js | 4 -- .../node_versioned_hooks_repo/package.json | 5 --- tests/languages/node_test.py | 41 +++++++++++++++++ tests/repository_test.py | 44 ------------------- 8 files changed, 41 insertions(+), 72 deletions(-) delete mode 100644 testing/resources/node_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/node_hooks_repo/bin/main.js delete mode 100644 testing/resources/node_hooks_repo/package.json delete mode 100644 testing/resources/node_versioned_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/node_versioned_hooks_repo/bin/main.js delete mode 100644 testing/resources/node_versioned_hooks_repo/package.json diff --git a/testing/resources/node_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/node_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 257698a44..000000000 --- a/testing/resources/node_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: foo - name: Foo - entry: foo - language: node - files: \.js$ diff --git a/testing/resources/node_hooks_repo/bin/main.js b/testing/resources/node_hooks_repo/bin/main.js deleted file mode 100644 index 8e0f025ab..000000000 --- a/testing/resources/node_hooks_repo/bin/main.js +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env node - -console.log('Hello World'); diff --git a/testing/resources/node_hooks_repo/package.json b/testing/resources/node_hooks_repo/package.json deleted file mode 100644 index 050b6300b..000000000 --- a/testing/resources/node_hooks_repo/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "foo", - "version": "0.0.1", - "bin": {"foo": "./bin/main.js"} -} diff --git a/testing/resources/node_versioned_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/node_versioned_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index e7ad5ea7b..000000000 --- a/testing/resources/node_versioned_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,6 +0,0 @@ -- id: versioned-node-hook - name: Versioned node hook - entry: versioned-node-hook - language: node - language_version: 9.3.0 - files: \.js$ diff --git a/testing/resources/node_versioned_hooks_repo/bin/main.js b/testing/resources/node_versioned_hooks_repo/bin/main.js deleted file mode 100644 index df12cbebe..000000000 --- a/testing/resources/node_versioned_hooks_repo/bin/main.js +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env node - -console.log(process.version); -console.log('Hello World'); diff --git a/testing/resources/node_versioned_hooks_repo/package.json b/testing/resources/node_versioned_hooks_repo/package.json deleted file mode 100644 index 18c7787c7..000000000 --- a/testing/resources/node_versioned_hooks_repo/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "versioned-node-hook", - "version": "0.0.1", - "bin": {"versioned-node-hook": "./bin/main.js"} -} diff --git a/tests/languages/node_test.py b/tests/languages/node_test.py index b69adfa67..cba0228b3 100644 --- a/tests/languages/node_test.py +++ b/tests/languages/node_test.py @@ -13,7 +13,9 @@ from pre_commit import parse_shebang from pre_commit.languages import node from pre_commit.prefix import Prefix +from pre_commit.store import _make_local_repo from pre_commit.util import cmd_output +from testing.language_helpers import run_language from testing.util import xfailif_windows @@ -109,3 +111,42 @@ def test_installs_without_links_outside_env(tmpdir): with node.in_env(prefix, 'system'): assert cmd_output('foo')[1] == 'success!\n' + + +def _make_hello_world(tmp_path): + package_json = '''\ +{"name": "t", "version": "0.0.1", "bin": {"node-hello": "./bin/main.js"}} +''' + tmp_path.joinpath('package.json').write_text(package_json) + bin_dir = tmp_path.joinpath('bin') + bin_dir.mkdir() + bin_dir.joinpath('main.js').write_text( + '#!/usr/bin/env node\n' + 'console.log("Hello World");\n', + ) + + +def test_node_hook_system(tmp_path): + _make_hello_world(tmp_path) + ret = run_language(tmp_path, node, 'node-hello') + assert ret == (0, b'Hello World\n') + + +def test_node_with_user_config_set(tmp_path): + cfg = tmp_path.joinpath('cfg') + cfg.write_text('cache=/dne\n') + with envcontext.envcontext((('NPM_CONFIG_USERCONFIG', str(cfg)),)): + test_node_hook_system(tmp_path) + + +@pytest.mark.parametrize('version', (C.DEFAULT, '18.13.0')) +def test_node_hook_versions(tmp_path, version): + _make_hello_world(tmp_path) + ret = run_language(tmp_path, node, 'node-hello', version=version) + assert ret == (0, b'Hello World\n') + + +def test_node_additional_deps(tmp_path): + _make_local_repo(str(tmp_path)) + ret, out = run_language(tmp_path, node, 'npm ls -g', deps=('lodash',)) + assert b' lodash@' in out diff --git a/tests/repository_test.py b/tests/repository_test.py index 2cd4c0fa5..b43b344c8 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -17,7 +17,6 @@ from pre_commit.hook import Hook from pre_commit.languages import golang from pre_commit.languages import helpers -from pre_commit.languages import node from pre_commit.languages import python from pre_commit.languages.all import languages from pre_commit.prefix import Prefix @@ -193,38 +192,6 @@ def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): ) -def test_run_a_node_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'node_hooks_repo', - 'foo', [os.devnull], b'Hello World\n', - ) - - -def test_run_a_node_hook_default_version(tempdir_factory, store): - # make sure that this continues to work for platforms where node is not - # installed at the system - with mock.patch.object( - node, - 'get_default_version', - return_value=C.DEFAULT, - ): - test_run_a_node_hook(tempdir_factory, store) - - -def test_run_versioned_node_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'node_versioned_hooks_repo', - 'versioned-node-hook', [os.devnull], b'v9.3.0\nHello World\n', - ) - - -def test_node_hook_with_npm_userconfig_set(tempdir_factory, store, tmpdir): - cfg = tmpdir.join('cfg') - cfg.write('cache=/dne\n') - with mock.patch.dict(os.environ, NPM_CONFIG_USERCONFIG=str(cfg)): - test_run_a_node_hook(tempdir_factory, store) - - def test_system_hook_with_spaces(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'system_hook_with_spaces_repo', @@ -482,17 +449,6 @@ def test_repository_state_compatibility(tempdir_factory, store, v): assert _hook_installed(hook) is True -def test_additional_node_dependencies_installed(tempdir_factory, store): - path = make_repo(tempdir_factory, 'node_hooks_repo') - config = make_config_from_repo(path) - # Careful to choose a small package that's not depped by npm - config['hooks'][0]['additional_dependencies'] = ['lodash'] - hook = _get_hook(config, store, 'foo') - with node.in_env(hook.prefix, hook.language_version): - output = cmd_output('npm', 'ls', '-g')[1] - assert 'lodash' in output - - def test_additional_golang_dependencies_installed( tempdir_factory, store, ): From d216cdd5c1eccab623a71aa8b58813e4850f167d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 1 Feb 2023 18:16:09 -0500 Subject: [PATCH 747/967] fix golang version regex in test --- tests/languages/golang_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py index 0219261fb..7c04255bc 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -1,9 +1,9 @@ from __future__ import annotations -import re from unittest import mock import pytest +import re_assert import pre_commit.constants as C from pre_commit.languages import golang @@ -40,4 +40,4 @@ def test_golang_infer_go_version_default(): version = ACTUAL_INFER_GO_VERSION(C.DEFAULT) assert version != C.DEFAULT - assert re.match(r'^\d+\.\d+\.\d+$', version) + re_assert.Matches(r'^\d+\.\d+(?:\.\d+)?$').assert_matches(version) From 7260d24d0fb0577f2111626b25d4f7bba56bfa5d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 1 Feb 2023 17:52:53 -0500 Subject: [PATCH 748/967] Revert "also ignore Gemfile in project" This reverts commit f4bd44996c888f48bc3a37d5ab19514325cb3f01. --- pre_commit/languages/ruby.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index b4d4b45af..4416f7280 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -39,7 +39,6 @@ def get_env_patch( ('GEM_HOME', os.path.join(venv, 'gems')), ('GEM_PATH', UNSET), ('BUNDLE_IGNORE_CONFIG', '1'), - ('BUNDLE_GEMFILE', os.devnull), ) if language_version == 'system': patches += ( From 1129e7d222fea31c9c536da0ae41610349854128 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 1 Feb 2023 17:58:08 -0500 Subject: [PATCH 749/967] fixup Gemfile in ruby tests --- tests/languages/ruby_test.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index b312c7fda..9cfaad5d0 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -123,8 +123,9 @@ def test_ruby_hook_language_version(tmp_path): def test_ruby_with_bundle_disable_shared_gems(tmp_path): workdir = tmp_path.joinpath('workdir') workdir.mkdir() - # this Gemfile is missing `source` - workdir.joinpath('Gemfile').write_text('gem "lol_hai"\n') + # this needs a `source` or there's a deprecation warning + # silencing this with `BUNDLE_GEMFILE` breaks some tools (#2739) + workdir.joinpath('Gemfile').write_text('source ""\ngem "lol_hai"\n') # this bundle config causes things to be written elsewhere bundle = workdir.joinpath('.bundle') bundle.mkdir() @@ -134,5 +135,5 @@ def test_ruby_with_bundle_disable_shared_gems(tmp_path): ) with cwd(workdir): - # `3.2.0` has new enough `gem` requiring `source` and reading `.bundle` + # `3.2.0` has new enough `gem` reading `.bundle` test_ruby_hook_language_version(tmp_path) From e846829992a84ce8066e6513a72a357709eec56c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 1 Feb 2023 18:21:18 -0500 Subject: [PATCH 750/967] v3.0.3 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0657e630..adf1e4b39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +3.0.3 - 2023-02-01 +================== + +### Fixes +- Revert "Prevent local `Gemfile` from interfering with hook execution.". + - #2739 issue by @Roguelazer. + - #2740 PR by @asottile. + 3.0.2 - 2023-01-29 ================== diff --git a/setup.cfg b/setup.cfg index 37511c09e..8eb9de7ae 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.0.2 +version = 3.0.3 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 7783a3e63a18ea3fb073eef5412b985153abdee8 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 2 Feb 2023 11:02:58 +0000 Subject: [PATCH 751/967] Add `--no-textconv` to `git diff` calls --- pre_commit/commands/run.py | 6 +++--- tests/commands/run_test.py | 41 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index e44e70364..a7eb4f45a 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -272,7 +272,8 @@ def _all_filenames(args: argparse.Namespace) -> Collection[str]: def _get_diff() -> bytes: _, out, _ = cmd_output_b( - 'git', 'diff', '--no-ext-diff', '--ignore-submodules', check=False, + 'git', 'diff', '--no-ext-diff', '--no-textconv', '--ignore-submodules', + check=False, ) return out @@ -326,8 +327,7 @@ def _has_unmerged_paths() -> bool: def _has_unstaged_config(config_file: str) -> bool: retcode, _, _ = cmd_output_b( - 'git', 'diff', '--no-ext-diff', '--exit-code', config_file, - check=False, + 'git', 'diff', '--quiet', '--no-ext-diff', config_file, check=False, ) # be explicit, other git errors don't mean it has an unstaged config. return retcode == 1 diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 03d741e06..f1085d9bb 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -766,6 +766,47 @@ def test_lots_of_files(store, tempdir_factory): ) +def test_no_textconv(cap_out, store, repo_with_passing_hook): + # git textconv filters can hide changes from hooks + with open('.gitattributes', 'w') as fp: + fp.write('*.jpeg diff=empty\n') + + with open('.git/config', 'a') as fp: + fp.write('[diff "empty"]\n') + fp.write('textconv = "true"\n') + + config = { + 'repo': 'local', + 'hooks': [ + { + 'id': 'extend-jpeg', + 'name': 'extend-jpeg', + 'language': 'system', + 'entry': ( + f'{shlex.quote(sys.executable)} -c "import sys; ' + 'open(sys.argv[1], \'ab\').write(b\'\\x00\')"' + ), + 'types': ['jpeg'], + }, + ], + } + add_config_to_repo(repo_with_passing_hook, config) + + stage_a_file('example.jpeg') + + _test_run( + cap_out, + store, + repo_with_passing_hook, + {}, + ( + b'Failed', + ), + expected_ret=1, + stage=False, + ) + + def test_stages(cap_out, store, repo_with_passing_hook): config = { 'repo': 'local', From 0359fae2da2aadb2fbd3afae1777edd3aa856cc9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 3 Feb 2023 12:07:23 -0500 Subject: [PATCH 752/967] v3.0.4 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index adf1e4b39..0998da98b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +3.0.4 - 2023-02-03 +================== + +### Fixes +- Fix hook diff detection for files affected by `--textconv`. + - #2743 PR by @adamchainz. + - #2743 issue by @adamchainz. + 3.0.3 - 2023-02-01 ================== diff --git a/setup.cfg b/setup.cfg index 8eb9de7ae..56b856cad 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.0.3 +version = 3.0.4 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 0c1267b214cee6da7337f7bcd42b89fd13015e26 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 4 Feb 2023 14:26:09 -0500 Subject: [PATCH 753/967] deprecate python_venv language --- pre_commit/commands/migrate_config.py | 9 +++++ pre_commit/repository.py | 9 +++++ .../.pre-commit-hooks.yaml | 5 --- .../resources/python_venv_hooks_repo/foo.py | 9 ----- .../resources/python_venv_hooks_repo/setup.py | 10 ------ tests/commands/migrate_config_test.py | 33 +++++++++++++++++++ tests/languages/all_test.py | 7 ++++ tests/repository_test.py | 20 ++++++++--- 8 files changed, 73 insertions(+), 29 deletions(-) delete mode 100644 testing/resources/python_venv_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/python_venv_hooks_repo/foo.py delete mode 100644 testing/resources/python_venv_hooks_repo/setup.py create mode 100644 tests/languages/all_test.py diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index 6f7af4eba..842fb3a7b 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -42,6 +42,14 @@ def _migrate_sha_to_rev(contents: str) -> str: return re.sub(r'(\n\s+)sha:', r'\1rev:', contents) +def _migrate_python_venv(contents: str) -> str: + return re.sub( + r'(\n\s+)language: python_venv\b', + r'\1language: python', + contents, + ) + + def migrate_config(config_file: str, quiet: bool = False) -> int: with open(config_file) as f: orig_contents = contents = f.read() @@ -55,6 +63,7 @@ def migrate_config(config_file: str, quiet: bool = False) -> int: contents = _migrate_map(contents) contents = _migrate_sha_to_rev(contents) + contents = _migrate_python_venv(contents) if contents != orig_contents: with open(config_file, 'w') as f: diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 616faf54c..308e80c70 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -3,6 +3,7 @@ import json import logging import os +import shlex from typing import Any from typing import Sequence @@ -68,6 +69,14 @@ def _hook_install(hook: Hook) -> None: logger.info('Once installed this environment will be reused.') logger.info('This may take a few minutes...') + if hook.language == 'python_venv': + logger.warning( + f'`repo: {hook.src}` uses deprecated `language: python_venv`. ' + f'This is an alias for `language: python`. ' + f'Often `pre-commit autoupdate --repo {shlex.quote(hook.src)}` ' + f'will fix this.', + ) + lang = languages[hook.language] assert lang.ENVIRONMENT_DIR is not None diff --git a/testing/resources/python_venv_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/python_venv_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index a666ed87a..000000000 --- a/testing/resources/python_venv_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: foo - name: Foo - entry: foo - language: python_venv - files: \.py$ diff --git a/testing/resources/python_venv_hooks_repo/foo.py b/testing/resources/python_venv_hooks_repo/foo.py deleted file mode 100644 index 40efde392..000000000 --- a/testing/resources/python_venv_hooks_repo/foo.py +++ /dev/null @@ -1,9 +0,0 @@ -from __future__ import annotations - -import sys - - -def main(): - print(repr(sys.argv[1:])) - print('Hello World') - return 0 diff --git a/testing/resources/python_venv_hooks_repo/setup.py b/testing/resources/python_venv_hooks_repo/setup.py deleted file mode 100644 index cff6cadf3..000000000 --- a/testing/resources/python_venv_hooks_repo/setup.py +++ /dev/null @@ -1,10 +0,0 @@ -from __future__ import annotations - -from setuptools import setup - -setup( - name='foo', - version='0.0.0', - py_modules=['foo'], - entry_points={'console_scripts': ['foo = foo:main']}, -) diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index fca1ad92f..ba1846360 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -134,6 +134,39 @@ def test_migrate_config_sha_to_rev(tmpdir): ) +def test_migrate_config_language_python_venv(tmp_path): + src = '''\ +repos: +- repo: local + hooks: + - id: example + name: example + entry: example + language: python_venv + - id: example + name: example + entry: example + language: system +''' + expected = '''\ +repos: +- repo: local + hooks: + - id: example + name: example + entry: example + language: python + - id: example + name: example + entry: example + language: system +''' + cfg = tmp_path.joinpath('cfg.yaml') + cfg.write_text(src) + assert migrate_config(str(cfg)) == 0 + assert cfg.read_text() == expected + + def test_migrate_config_invalid_yaml(tmpdir): contents = '[' cfg = tmpdir.join(C.CONFIG_FILE) diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py new file mode 100644 index 000000000..33b8925fb --- /dev/null +++ b/tests/languages/all_test.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from pre_commit.languages.all import languages + + +def test_python_venv_is_an_alias_to_python(): + assert languages['python_venv'] is languages['python'] diff --git a/tests/repository_test.py b/tests/repository_test.py index b43b344c8..9ec2d5493 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -129,11 +129,21 @@ def test_python_hook_weird_setup_cfg(in_git_dir, tempdir_factory, store): ) -def test_python_venv(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'python_venv_hooks_repo', - 'foo', [os.devnull], - f'[{os.devnull!r}]\nHello World\n'.encode(), +def test_python_venv_deprecation(store, caplog): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'example', + 'name': 'example', + 'language': 'python_venv', + 'entry': 'echo hi', + }], + } + _get_hook(config, store, 'example') + assert caplog.messages[-1] == ( + '`repo: local` uses deprecated `language: python_venv`. ' + 'This is an alias for `language: python`. ' + 'Often `pre-commit autoupdate --repo local` will fix this.' ) From 0afb95ccca2f590bf45f45bcafb8ca792ce66423 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 4 Feb 2023 16:50:40 -0500 Subject: [PATCH 754/967] test docker and docker_image directly --- pre_commit/languages/docker.py | 3 +- testing/language_helpers.py | 7 ++-- .../docker_hooks_repo/.pre-commit-hooks.yaml | 17 -------- .../resources/docker_hooks_repo/Dockerfile | 3 -- .../.pre-commit-hooks.yaml | 8 ---- testing/util.py | 15 ------- tests/languages/docker_image_test.py | 27 +++++++++++++ tests/languages/docker_test.py | 14 +++++++ tests/repository_test.py | 40 ------------------- 9 files changed, 46 insertions(+), 88 deletions(-) delete mode 100644 testing/resources/docker_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/docker_hooks_repo/Dockerfile delete mode 100644 testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml create mode 100644 tests/languages/docker_image_test.py diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index e80c95978..2212c5ccb 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -138,9 +138,8 @@ def run_hook( entry_exe, *cmd_rest = helpers.hook_cmd(entry, args) entry_tag = ('--entrypoint', entry_exe, docker_tag(prefix)) - cmd = (*docker_cmd(), *entry_tag, *cmd_rest) return helpers.run_xargs( - cmd, + (*docker_cmd(), *entry_tag, *cmd_rest), file_args, require_serial=require_serial, color=color, diff --git a/testing/language_helpers.py b/testing/language_helpers.py index b9c538403..0964fbb44 100644 --- a/testing/language_helpers.py +++ b/testing/language_helpers.py @@ -20,9 +20,10 @@ def run_language( prefix = Prefix(str(path)) version = version or language.get_default_version() - language.install_environment(prefix, version, deps) - health_error = language.health_check(prefix, version) - assert health_error is None, health_error + if language.ENVIRONMENT_DIR is not None: + language.install_environment(prefix, version, deps) + health_error = language.health_check(prefix, version) + assert health_error is None, health_error with language.in_env(prefix, version): ret, out = language.run_hook( prefix, diff --git a/testing/resources/docker_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/docker_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 529573965..000000000 --- a/testing/resources/docker_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,17 +0,0 @@ -- id: docker-hook - name: Docker test hook - entry: echo - language: docker - files: \.txt$ - -- id: docker-hook-arg - name: Docker test hook - entry: echo -n - language: docker - files: \.txt$ - -- id: docker-hook-failing - name: Docker test hook with nonzero exit code - entry: bork - language: docker - files: \.txt$ diff --git a/testing/resources/docker_hooks_repo/Dockerfile b/testing/resources/docker_hooks_repo/Dockerfile deleted file mode 100644 index 0bd1de0cf..000000000 --- a/testing/resources/docker_hooks_repo/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM ubuntu:focal - -CMD ["echo", "This is overwritten by the .pre-commit-hooks.yaml 'entry'"] diff --git a/testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index e9fb24569..000000000 --- a/testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,8 +0,0 @@ -- id: echo-entrypoint - name: echo (via --entrypoint) - language: docker_image - entry: --entrypoint echo ubuntu:focal -- id: echo-cmd - name: echo (via cmd) - language: docker_image - entry: ubuntu:focal echo diff --git a/testing/util.py b/testing/util.py index b6c3804e6..7c68d0eee 100644 --- a/testing/util.py +++ b/testing/util.py @@ -6,24 +6,13 @@ import pytest -from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output -from pre_commit.util import cmd_output_b from testing.auto_namedtuple import auto_namedtuple TESTING_DIR = os.path.abspath(os.path.dirname(__file__)) -def docker_is_running() -> bool: # pragma: win32 no cover - try: - cmd_output_b('docker', 'ps') - except CalledProcessError: # pragma: no cover - return False - else: - return True - - def get_resource_path(path): return os.path.join(TESTING_DIR, 'resources', path) @@ -41,10 +30,6 @@ def cmd_output_mocked_pre_commit_home( return ret, out.replace('\r\n', '\n'), None -skipif_cant_run_docker = pytest.mark.skipif( - os.name == 'nt' or not docker_is_running(), - reason="Docker isn't running or can't be accessed", -) xfailif_windows = pytest.mark.xfail(os.name == 'nt', reason='windows') diff --git a/tests/languages/docker_image_test.py b/tests/languages/docker_image_test.py new file mode 100644 index 000000000..7993c11a8 --- /dev/null +++ b/tests/languages/docker_image_test.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from pre_commit.languages import docker_image +from testing.language_helpers import run_language +from testing.util import xfailif_windows + + +@xfailif_windows # pragma: win32 no cover +def test_docker_image_hook_via_entrypoint(tmp_path): + ret = run_language( + tmp_path, + docker_image, + '--entrypoint echo ubuntu:22.04', + args=('hello hello world',), + ) + assert ret == (0, b'hello hello world\n') + + +@xfailif_windows # pragma: win32 no cover +def test_docker_image_hook_via_args(tmp_path): + ret = run_language( + tmp_path, + docker_image, + 'ubuntu:22.04 echo', + args=('hello hello world',), + ) + assert ret == (0, b'hello hello world\n') diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 5f7c85e71..836382a8a 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -11,6 +11,8 @@ from pre_commit.languages import docker from pre_commit.util import CalledProcessError +from testing.language_helpers import run_language +from testing.util import xfailif_windows DOCKER_CGROUP_EXAMPLE = b'''\ 12:hugetlb:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 @@ -181,3 +183,15 @@ def test_get_docker_path_in_docker_docker_in_docker(in_docker): err = CalledProcessError(1, (), b'', b'') with mock.patch.object(docker, 'cmd_output_b', side_effect=err): assert docker._get_docker_path('/project') == '/project' + + +@xfailif_windows # pragma: win32 no cover +def test_docker_hook(tmp_path): + dockerfile = '''\ +FROM ubuntu:22.04 +CMD ["echo", "This is overwritten by the entry"'] +''' + tmp_path.joinpath('Dockerfile').write_text(dockerfile) + + ret = run_language(tmp_path, docker, 'echo hello hello world') + assert ret == (0, b'hello hello world\n') diff --git a/tests/repository_test.py b/tests/repository_test.py index 9ec2d5493..a4dcda5b3 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -30,7 +30,6 @@ from testing.fixtures import modify_manifest from testing.util import cwd from testing.util import get_resource_path -from testing.util import skipif_cant_run_docker def _norm_out(b): @@ -163,45 +162,6 @@ def test_language_versioned_python_hook(tempdir_factory, store): ) -@skipif_cant_run_docker # pragma: win32 no cover -def test_run_a_docker_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'docker_hooks_repo', - 'docker-hook', - ['Hello World from docker'], b'Hello World from docker\n', - ) - - -@skipif_cant_run_docker # pragma: win32 no cover -def test_run_a_docker_hook_with_entry_args(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'docker_hooks_repo', - 'docker-hook-arg', - ['Hello World from docker'], b'Hello World from docker', - ) - - -@skipif_cant_run_docker # pragma: win32 no cover -def test_run_a_failing_docker_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'docker_hooks_repo', - 'docker-hook-failing', - ['Hello World from docker'], - mock.ANY, # an error message about `bork` not existing - expected_return_code=127, - ) - - -@skipif_cant_run_docker # pragma: win32 no cover -@pytest.mark.parametrize('hook_id', ('echo-entrypoint', 'echo-cmd')) -def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): - _test_hook_repo( - tempdir_factory, store, 'docker_image_hooks_repo', - hook_id, - ['Hello World from docker'], b'Hello World from docker\n', - ) - - def test_system_hook_with_spaces(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'system_hook_with_spaces_repo', From 6804100701a40c7defdbd5027e459385ceeba8f2 Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Mon, 6 Feb 2023 12:24:30 -0600 Subject: [PATCH 755/967] test golang directly --- .../golang_hooks_repo/.pre-commit-hooks.yaml | 5 - testing/resources/golang_hooks_repo/go.mod | 5 - testing/resources/golang_hooks_repo/go.sum | 2 - .../golang-hello-world/main.go | 23 ---- tests/languages/golang_test.py | 93 +++++++++++++ tests/repository_test.py | 126 ------------------ tests/store_test.py | 24 ++++ 7 files changed, 117 insertions(+), 161 deletions(-) delete mode 100644 testing/resources/golang_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/golang_hooks_repo/go.mod delete mode 100644 testing/resources/golang_hooks_repo/go.sum delete mode 100644 testing/resources/golang_hooks_repo/golang-hello-world/main.go diff --git a/testing/resources/golang_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/golang_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 206733bb6..000000000 --- a/testing/resources/golang_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: golang-hook - name: golang example hook - entry: golang-hello-world - language: golang - files: '' diff --git a/testing/resources/golang_hooks_repo/go.mod b/testing/resources/golang_hooks_repo/go.mod deleted file mode 100644 index f37d4b674..000000000 --- a/testing/resources/golang_hooks_repo/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module golang-hello-world - -go 1.18 - -require github.com/BurntSushi/toml v1.1.0 diff --git a/testing/resources/golang_hooks_repo/go.sum b/testing/resources/golang_hooks_repo/go.sum deleted file mode 100644 index ec0c385a0..000000000 --- a/testing/resources/golang_hooks_repo/go.sum +++ /dev/null @@ -1,2 +0,0 @@ -github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= -github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= diff --git a/testing/resources/golang_hooks_repo/golang-hello-world/main.go b/testing/resources/golang_hooks_repo/golang-hello-world/main.go deleted file mode 100644 index 168574384..000000000 --- a/testing/resources/golang_hooks_repo/golang-hello-world/main.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - - -import ( - "fmt" - "runtime" - "github.com/BurntSushi/toml" - "os" -) - -type Config struct { - What string -} - -func main() { - message := runtime.Version() - if len(os.Args) > 1 { - message = os.Args[1] - } - var conf Config - toml.Decode("What = 'world'\n", &conf) - fmt.Printf("hello %v from %s\n", conf.What, message) -} diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py index 7c04255bc..f5f9985b8 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -6,8 +6,11 @@ import re_assert import pre_commit.constants as C +from pre_commit.envcontext import envcontext from pre_commit.languages import golang from pre_commit.languages import helpers +from pre_commit.store import _make_local_repo +from testing.language_helpers import run_language ACTUAL_GET_DEFAULT_VERSION = golang.get_default_version.__wrapped__ @@ -41,3 +44,93 @@ def test_golang_infer_go_version_default(): assert version != C.DEFAULT re_assert.Matches(r'^\d+\.\d+(?:\.\d+)?$').assert_matches(version) + + +def _make_hello_world(tmp_path): + go_mod = '''\ +module golang-hello-world + +go 1.18 + +require github.com/BurntSushi/toml v1.1.0 +''' + go_sum = '''\ +github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= +github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +''' # noqa: E501 + hello_world_go = '''\ +package main + + +import ( + "fmt" + "github.com/BurntSushi/toml" +) + +type Config struct { + What string +} + +func main() { + var conf Config + toml.Decode("What = 'world'\\n", &conf) + fmt.Printf("hello %v\\n", conf.What) +} +''' + tmp_path.joinpath('go.mod').write_text(go_mod) + tmp_path.joinpath('go.sum').write_text(go_sum) + mod_dir = tmp_path.joinpath('golang-hello-world') + mod_dir.mkdir() + main_file = mod_dir.joinpath('main.go') + main_file.write_text(hello_world_go) + + +def test_golang_system(tmp_path): + _make_hello_world(tmp_path) + + ret = run_language(tmp_path, golang, 'golang-hello-world') + assert ret == (0, b'hello world\n') + + +def test_golang_default_version(tmp_path): + _make_hello_world(tmp_path) + + ret = run_language( + tmp_path, + golang, + 'golang-hello-world', + version=C.DEFAULT, + ) + assert ret == (0, b'hello world\n') + + +def test_golang_versioned(tmp_path): + _make_local_repo(str(tmp_path)) + + ret, out = run_language( + tmp_path, + golang, + 'go version', + version='1.18.4', + ) + + assert ret == 0 + assert out.startswith(b'go version go1.18.4') + + +def test_local_golang_additional_deps(tmp_path): + _make_local_repo(str(tmp_path)) + + ret = run_language( + tmp_path, + golang, + 'hello', + deps=('golang.org/x/example/hello@latest',), + ) + + assert ret == (0, b'Hello, Go examples!\n') + + +def test_golang_hook_still_works_when_gobin_is_set(tmp_path): + with envcontext((('GOBIN', str(tmp_path.joinpath('gobin'))),)): + test_golang_system(tmp_path) diff --git a/tests/repository_test.py b/tests/repository_test.py index a4dcda5b3..0c9bba741 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -10,12 +10,9 @@ import re_assert import pre_commit.constants as C -from pre_commit import git from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest -from pre_commit.envcontext import envcontext from pre_commit.hook import Hook -from pre_commit.languages import golang from pre_commit.languages import helpers from pre_commit.languages import python from pre_commit.languages.all import languages @@ -169,92 +166,6 @@ def test_system_hook_with_spaces(tempdir_factory, store): ) -def test_golang_system_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'golang_hooks_repo', - 'golang-hook', ['system'], b'hello world from system\n', - config_kwargs={ - 'hooks': [{ - 'id': 'golang-hook', - 'language_version': 'system', - }], - }, - ) - - -def test_golang_versioned_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'golang_hooks_repo', - 'golang-hook', [], b'hello world from go1.18.4\n', - config_kwargs={ - 'hooks': [{ - 'id': 'golang-hook', - 'language_version': '1.18.4', - }], - }, - ) - - -def test_golang_hook_still_works_when_gobin_is_set(tempdir_factory, store): - gobin_dir = tempdir_factory.get() - with envcontext((('GOBIN', gobin_dir),)): - test_golang_system_hook(tempdir_factory, store) - assert os.listdir(gobin_dir) == [] - - -def test_golang_with_recursive_submodule(tmpdir, tempdir_factory, store): - sub_go = '''\ -package sub - -import "fmt" - -func Func() { - fmt.Println("hello hello world") -} -''' - sub = tmpdir.join('sub').ensure_dir() - sub.join('sub.go').write(sub_go) - cmd_output('git', '-C', str(sub), 'init', '.') - cmd_output('git', '-C', str(sub), 'add', '.') - git.commit(str(sub)) - - pre_commit_hooks = '''\ -- id: example - name: example - entry: example - language: golang - verbose: true -''' - go_mod = '''\ -module github.com/asottile/example - -go 1.14 -''' - main_go = '''\ -package main - -import "github.com/asottile/example/sub" - -func main() { - sub.Func() -} -''' - repo = tmpdir.join('repo').ensure_dir() - repo.join('.pre-commit-hooks.yaml').write(pre_commit_hooks) - repo.join('go.mod').write(go_mod) - repo.join('main.go').write(main_go) - cmd_output('git', '-C', str(repo), 'init', '.') - cmd_output('git', '-C', str(repo), 'add', '.') - cmd_output('git', '-C', str(repo), 'submodule', 'add', str(sub), 'sub') - git.commit(str(repo)) - - config = make_config_from_repo(str(repo)) - hook = _get_hook(config, store, 'example') - ret, out = _hook_run(hook, (), color=False) - assert ret == 0 - assert _norm_out(out) == b'hello hello world\n' - - def test_missing_executable(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'not_found_exe', @@ -419,43 +330,6 @@ def test_repository_state_compatibility(tempdir_factory, store, v): assert _hook_installed(hook) is True -def test_additional_golang_dependencies_installed( - tempdir_factory, store, -): - path = make_repo(tempdir_factory, 'golang_hooks_repo') - config = make_config_from_repo(path) - # A small go package - deps = ['golang.org/x/example/hello@latest'] - config['hooks'][0]['additional_dependencies'] = deps - hook = _get_hook(config, store, 'golang-hook') - envdir = helpers.environment_dir( - hook.prefix, - golang.ENVIRONMENT_DIR, - golang.get_default_version(), - ) - binaries = os.listdir(os.path.join(envdir, 'bin')) - # normalize for windows - binaries = [os.path.splitext(binary)[0] for binary in binaries] - assert 'hello' in binaries - - -def test_local_golang_additional_dependencies(store): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'hello', - 'name': 'hello', - 'entry': 'hello', - 'language': 'golang', - 'additional_dependencies': ['golang.org/x/example/hello@latest'], - }], - } - hook = _get_hook(config, store, 'hello') - ret, out = _hook_run(hook, (), color=False) - assert ret == 0 - assert _norm_out(out) == b'Hello, Go examples!\n' - - def test_fail_hooks(store): config = { 'repo': 'local', diff --git a/tests/store_test.py b/tests/store_test.py index c42ce6537..146eac416 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -246,3 +246,27 @@ def _chmod_minus_w(p): # should be skipped due to readonly store.mark_config_used(str(cfg)) assert store.select_all_configs() == [] + + +def test_clone_with_recursive_submodules(store, tmp_path): + sub = tmp_path.joinpath('sub') + sub.mkdir() + sub.joinpath('submodule').write_text('i am a submodule') + cmd_output('git', '-C', str(sub), 'init', '.') + cmd_output('git', '-C', str(sub), 'add', '.') + git.commit(str(sub)) + + repo = tmp_path.joinpath('repo') + repo.mkdir() + repo.joinpath('repository').write_text('i am a repo') + cmd_output('git', '-C', str(repo), 'init', '.') + cmd_output('git', '-C', str(repo), 'add', '.') + cmd_output('git', '-C', str(repo), 'submodule', 'add', str(sub), 'sub') + git.commit(str(repo)) + + rev = git.head_rev(str(repo)) + ret = store.clone(str(repo), rev) + + assert os.path.exists(ret) + assert os.path.exists(os.path.join(ret, str(repo), 'repository')) + assert os.path.exists(os.path.join(ret, str(sub), 'submodule')) From 915b930a5d0c894a4b0d2a6957f833179255cd42 Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Tue, 7 Feb 2023 21:22:26 -0600 Subject: [PATCH 756/967] test dotnet directly --- pre_commit/languages/dotnet.py | 4 - .../.pre-commit-hooks.yaml | 12 -- .../dotnet_hooks_combo_repo.sln | 28 ---- .../dotnet_hooks_combo_repo/proj1/Program.cs | 12 -- .../proj1/proj1.csproj | 12 -- .../dotnet_hooks_combo_repo/proj2/Program.cs | 12 -- .../proj2/proj2.csproj | 12 -- .../.gitignore | 3 - .../.pre-commit-hooks.yaml | 5 - .../Program.cs | 12 -- .../dotnet_hooks_csproj_prefix_repo.csproj | 9 - .../dotnet_hooks_csproj_repo/.gitignore | 3 - .../.pre-commit-hooks.yaml | 5 - .../dotnet_hooks_csproj_repo/Program.cs | 12 -- .../dotnet_hooks_csproj_repo.csproj | 9 - .../dotnet_hooks_sln_repo/.gitignore | 3 - .../.pre-commit-hooks.yaml | 5 - .../dotnet_hooks_sln_repo/Program.cs | 12 -- .../dotnet_hooks_sln_repo.csproj | 9 - .../dotnet_hooks_sln_repo.sln | 34 ---- tests/languages/dotnet_test.py | 154 ++++++++++++++++++ tests/repository_test.py | 16 -- 22 files changed, 154 insertions(+), 229 deletions(-) delete mode 100644 testing/resources/dotnet_hooks_combo_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/dotnet_hooks_combo_repo/dotnet_hooks_combo_repo.sln delete mode 100644 testing/resources/dotnet_hooks_combo_repo/proj1/Program.cs delete mode 100644 testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj delete mode 100644 testing/resources/dotnet_hooks_combo_repo/proj2/Program.cs delete mode 100644 testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj delete mode 100644 testing/resources/dotnet_hooks_csproj_prefix_repo/.gitignore delete mode 100644 testing/resources/dotnet_hooks_csproj_prefix_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/dotnet_hooks_csproj_prefix_repo/Program.cs delete mode 100644 testing/resources/dotnet_hooks_csproj_prefix_repo/dotnet_hooks_csproj_prefix_repo.csproj delete mode 100644 testing/resources/dotnet_hooks_csproj_repo/.gitignore delete mode 100644 testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/dotnet_hooks_csproj_repo/Program.cs delete mode 100644 testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj delete mode 100644 testing/resources/dotnet_hooks_sln_repo/.gitignore delete mode 100644 testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/dotnet_hooks_sln_repo/Program.cs delete mode 100644 testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj delete mode 100644 testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.sln diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index 4c3955e85..05d4ce32c 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -109,7 +109,3 @@ def install_environment( tool_id, ), ) - - # Clean the git dir, ignoring the environment dir - clean_cmd = ('git', 'clean', '-ffxd', '-e', f'{ENVIRONMENT_DIR}-*') - helpers.run_setup_cmd(prefix, clean_cmd) diff --git a/testing/resources/dotnet_hooks_combo_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_combo_repo/.pre-commit-hooks.yaml deleted file mode 100644 index f221854a4..000000000 --- a/testing/resources/dotnet_hooks_combo_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,12 +0,0 @@ -- id: dotnet-example-hook - name: Test Project 1 - description: Test Project 1 - entry: proj1 - language: dotnet - stages: [commit] -- id: proj2 - name: Test Project 2 - description: Test Project 2 - entry: proj2 - language: dotnet - stages: [commit] diff --git a/testing/resources/dotnet_hooks_combo_repo/dotnet_hooks_combo_repo.sln b/testing/resources/dotnet_hooks_combo_repo/dotnet_hooks_combo_repo.sln deleted file mode 100644 index edb0fcbc5..000000000 --- a/testing/resources/dotnet_hooks_combo_repo/dotnet_hooks_combo_repo.sln +++ /dev/null @@ -1,28 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30114.105 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "proj1", "proj1\proj1.csproj", "{38A939C3-DEA4-47D7-9B75-0418C4249662}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "proj2", "proj2\proj2.csproj", "{4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {38A939C3-DEA4-47D7-9B75-0418C4249662}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {38A939C3-DEA4-47D7-9B75-0418C4249662}.Debug|Any CPU.Build.0 = Debug|Any CPU - {38A939C3-DEA4-47D7-9B75-0418C4249662}.Release|Any CPU.ActiveCfg = Release|Any CPU - {38A939C3-DEA4-47D7-9B75-0418C4249662}.Release|Any CPU.Build.0 = Release|Any CPU - {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal diff --git a/testing/resources/dotnet_hooks_combo_repo/proj1/Program.cs b/testing/resources/dotnet_hooks_combo_repo/proj1/Program.cs deleted file mode 100644 index 03876f5cd..000000000 --- a/testing/resources/dotnet_hooks_combo_repo/proj1/Program.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace proj1 -{ - class Program - { - static void Main(string[] args) - { - Console.Write("Hello from dotnet!\n"); - } - } -} diff --git a/testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj b/testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj deleted file mode 100644 index 861ced6d9..000000000 --- a/testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - Exe - net6 - - true - proj1 - ./nupkg - - - diff --git a/testing/resources/dotnet_hooks_combo_repo/proj2/Program.cs b/testing/resources/dotnet_hooks_combo_repo/proj2/Program.cs deleted file mode 100644 index 47a99a358..000000000 --- a/testing/resources/dotnet_hooks_combo_repo/proj2/Program.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace proj2 -{ - class Program - { - static void Main(string[] args) - { - Console.WriteLine("Hello World!"); - } - } -} diff --git a/testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj b/testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj deleted file mode 100644 index dfce2cad1..000000000 --- a/testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - Exe - net6 - - true - proj2 - ./nupkg - - - diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/.gitignore b/testing/resources/dotnet_hooks_csproj_prefix_repo/.gitignore deleted file mode 100644 index edcd28f4a..000000000 --- a/testing/resources/dotnet_hooks_csproj_prefix_repo/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -bin/ -obj/ -nupkg/ diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_csproj_prefix_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 6626627d7..000000000 --- a/testing/resources/dotnet_hooks_csproj_prefix_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: dotnet-example-hook - name: dotnet example hook - entry: testeroni.tool - language: dotnet - files: '' diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/Program.cs b/testing/resources/dotnet_hooks_csproj_prefix_repo/Program.cs deleted file mode 100644 index 1456e8ef2..000000000 --- a/testing/resources/dotnet_hooks_csproj_prefix_repo/Program.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace dotnet_hooks_repo -{ - class Program - { - static void Main(string[] args) - { - Console.WriteLine("Hello from dotnet!"); - } - } -} diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/dotnet_hooks_csproj_prefix_repo.csproj b/testing/resources/dotnet_hooks_csproj_prefix_repo/dotnet_hooks_csproj_prefix_repo.csproj deleted file mode 100644 index 754b76006..000000000 --- a/testing/resources/dotnet_hooks_csproj_prefix_repo/dotnet_hooks_csproj_prefix_repo.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - Exe - net7.0 - true - testeroni.tool - ./nupkg - - diff --git a/testing/resources/dotnet_hooks_csproj_repo/.gitignore b/testing/resources/dotnet_hooks_csproj_repo/.gitignore deleted file mode 100644 index edcd28f4a..000000000 --- a/testing/resources/dotnet_hooks_csproj_repo/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -bin/ -obj/ -nupkg/ diff --git a/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 0f514c116..000000000 --- a/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: dotnet-example-hook - name: dotnet example hook - entry: testeroni - language: dotnet - files: '' diff --git a/testing/resources/dotnet_hooks_csproj_repo/Program.cs b/testing/resources/dotnet_hooks_csproj_repo/Program.cs deleted file mode 100644 index 1456e8ef2..000000000 --- a/testing/resources/dotnet_hooks_csproj_repo/Program.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace dotnet_hooks_repo -{ - class Program - { - static void Main(string[] args) - { - Console.WriteLine("Hello from dotnet!"); - } - } -} diff --git a/testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj b/testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj deleted file mode 100644 index fa9879b0d..000000000 --- a/testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - Exe - net6 - true - testeroni - ./nupkg - - diff --git a/testing/resources/dotnet_hooks_sln_repo/.gitignore b/testing/resources/dotnet_hooks_sln_repo/.gitignore deleted file mode 100644 index edcd28f4a..000000000 --- a/testing/resources/dotnet_hooks_sln_repo/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -bin/ -obj/ -nupkg/ diff --git a/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 0f514c116..000000000 --- a/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: dotnet-example-hook - name: dotnet example hook - entry: testeroni - language: dotnet - files: '' diff --git a/testing/resources/dotnet_hooks_sln_repo/Program.cs b/testing/resources/dotnet_hooks_sln_repo/Program.cs deleted file mode 100644 index 04ad4e0cc..000000000 --- a/testing/resources/dotnet_hooks_sln_repo/Program.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace dotnet_hooks_sln_repo -{ - class Program - { - static void Main(string[] args) - { - Console.WriteLine("Hello from dotnet!"); - } - } -} diff --git a/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj b/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj deleted file mode 100644 index a4e2d0058..000000000 --- a/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - Exe - net6 - true - testeroni - ./nupkg - - diff --git a/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.sln b/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.sln deleted file mode 100644 index 87d2afbaf..000000000 --- a/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.sln +++ /dev/null @@ -1,34 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26124.0 -MinimumVisualStudioVersion = 15.0.26124.0 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet_hooks_sln_repo", "dotnet_hooks_sln_repo.csproj", "{6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x64.ActiveCfg = Debug|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x64.Build.0 = Debug|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x86.ActiveCfg = Debug|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x86.Build.0 = Debug|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|Any CPU.Build.0 = Release|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x64.ActiveCfg = Release|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x64.Build.0 = Release|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x86.ActiveCfg = Release|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal diff --git a/tests/languages/dotnet_test.py b/tests/languages/dotnet_test.py index e69de29bb..470c03b22 100644 --- a/tests/languages/dotnet_test.py +++ b/tests/languages/dotnet_test.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +from pre_commit.languages import dotnet +from testing.language_helpers import run_language + + +def _write_program_cs(tmp_path): + program_cs = '''\ +using System; + +namespace dotnet_tests +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello from dotnet!"); + } + } +} +''' + tmp_path.joinpath('Program.cs').write_text(program_cs) + + +def _csproj(tool_name): + return f'''\ + + + Exe + net6 + true + {tool_name} + ./nupkg + + +''' + + +def test_dotnet_csproj(tmp_path): + csproj = _csproj('testeroni') + _write_program_cs(tmp_path) + tmp_path.joinpath('dotnet_csproj.csproj').write_text(csproj) + ret = run_language(tmp_path, dotnet, 'testeroni') + assert ret == (0, b'Hello from dotnet!\n') + + +def test_dotnet_csproj_prefix(tmp_path): + csproj = _csproj('testeroni.tool') + _write_program_cs(tmp_path) + tmp_path.joinpath('dotnet_hooks_csproj_prefix.csproj').write_text(csproj) + ret = run_language(tmp_path, dotnet, 'testeroni.tool') + assert ret == (0, b'Hello from dotnet!\n') + + +def test_dotnet_sln(tmp_path): + csproj = _csproj('testeroni') + sln = '''\ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet_hooks_sln_repo", "dotnet_hooks_sln_repo.csproj", "{6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x64.ActiveCfg = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x64.Build.0 = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x86.ActiveCfg = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x86.Build.0 = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|Any CPU.Build.0 = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x64.ActiveCfg = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x64.Build.0 = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x86.ActiveCfg = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal +''' # noqa: E501 + _write_program_cs(tmp_path) + tmp_path.joinpath('dotnet_hooks_sln_repo.csproj').write_text(csproj) + tmp_path.joinpath('dotnet_hooks_sln_repo.sln').write_text(sln) + + ret = run_language(tmp_path, dotnet, 'testeroni') + assert ret == (0, b'Hello from dotnet!\n') + + +def _setup_dotnet_combo(tmp_path): + sln = '''\ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "proj1", "proj1\\proj1.csproj", "{38A939C3-DEA4-47D7-9B75-0418C4249662}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "proj2", "proj2\\proj2.csproj", "{4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Release|Any CPU.Build.0 = Release|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal +''' # noqa: E501 + tmp_path.joinpath('dotnet_hooks_combo_repo.sln').write_text(sln) + + csproj1 = _csproj('proj1') + proj1 = tmp_path.joinpath('proj1') + proj1.mkdir() + proj1.joinpath('proj1.csproj').write_text(csproj1) + _write_program_cs(proj1) + + csproj2 = _csproj('proj2') + proj2 = tmp_path.joinpath('proj2') + proj2.mkdir() + proj2.joinpath('proj2.csproj').write_text(csproj2) + _write_program_cs(proj2) + + +def test_dotnet_combo_proj1(tmp_path): + _setup_dotnet_combo(tmp_path) + ret = run_language(tmp_path, dotnet, 'proj1') + assert ret == (0, b'Hello from dotnet!\n') + + +def test_dotnet_combo_proj2(tmp_path): + _setup_dotnet_combo(tmp_path) + ret = run_language(tmp_path, dotnet, 'proj2') + assert ret == (0, b'Hello from dotnet!\n') diff --git a/tests/repository_test.py b/tests/repository_test.py index 0c9bba741..9e2f1e519 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -625,22 +625,6 @@ def test_manifest_hooks(tempdir_factory, store): ) -@pytest.mark.parametrize( - 'repo', - ( - 'dotnet_hooks_csproj_repo', - 'dotnet_hooks_sln_repo', - 'dotnet_hooks_combo_repo', - 'dotnet_hooks_csproj_prefix_repo', - ), -) -def test_dotnet_hook(tempdir_factory, store, repo): - _test_hook_repo( - tempdir_factory, store, repo, - 'dotnet-example-hook', [], b'Hello from dotnet!\n', - ) - - def test_non_installable_hook_error_for_language_version(store, caplog): config = { 'repo': 'local', From abbfb2e9b9195f6ae03441a0d69e4d2f8575d416 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 8 Feb 2023 06:43:04 +0000 Subject: [PATCH 757/967] List golang as first-class language --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9bcb79ed..ab3a92989 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,10 +64,10 @@ to implement. The current implemented languages are at varying levels: - 0th class - pre-commit does not require any dependencies for these languages as they're not actually languages (current examples: fail, pygrep) - 1st class - pre-commit will bootstrap a full interpreter requiring nothing to - be installed globally (current examples: node, ruby, rust) + be installed globally (current examples: go, node, ruby, rust) - 2nd class - pre-commit requires the user to install the language globally but - will install tools in an isolated fashion (current examples: python, go, - swift, docker). + will install tools in an isolated fashion (current examples: python, swift, + docker). - 3rd class - pre-commit requires the user to install both the tool and the language globally (current examples: script, system) From 563507937324d8214a82f3cfd6199ea4ace875d0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 8 Feb 2023 11:20:30 -0500 Subject: [PATCH 758/967] force the issue template more --- .../ISSUE_TEMPLATE/{bug.yaml => 00_bug.yaml} | 6 +++ .github/ISSUE_TEMPLATE/01_feature.yaml | 38 +++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yaml | 6 +++ 3 files changed, 50 insertions(+) rename .github/ISSUE_TEMPLATE/{bug.yaml => 00_bug.yaml} (87%) create mode 100644 .github/ISSUE_TEMPLATE/01_feature.yaml create mode 100644 .github/ISSUE_TEMPLATE/config.yaml diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/00_bug.yaml similarity index 87% rename from .github/ISSUE_TEMPLATE/bug.yaml rename to .github/ISSUE_TEMPLATE/00_bug.yaml index 96cd6c75c..980f7afee 100644 --- a/.github/ISSUE_TEMPLATE/bug.yaml +++ b/.github/ISSUE_TEMPLATE/00_bug.yaml @@ -16,6 +16,12 @@ body: placeholder: ... validations: required: true + - type: markdown + attributes: + value: | + 95% of issues created are duplicates. + please try extra hard to find them first. + it's very unlikely your problem is unique. - type: textarea id: freeform attributes: diff --git a/.github/ISSUE_TEMPLATE/01_feature.yaml b/.github/ISSUE_TEMPLATE/01_feature.yaml new file mode 100644 index 000000000..c7ddc84cd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01_feature.yaml @@ -0,0 +1,38 @@ +name: feature request +description: something new +body: + - type: markdown + attributes: + value: | + this is for issues for `pre-commit` (the framework). + if you are reporting an issue for [pre-commit.ci] please report it at [pre-commit-ci/issues] + + [pre-commit.ci]: https://pre-commit.ci + [pre-commit-ci/issues]: https://github.com/pre-commit-ci/issues + - type: input + id: search + attributes: + label: search you tried in the issue tracker + placeholder: ... + validations: + required: true + - type: markdown + attributes: + value: | + 95% of issues created are duplicates. + please try extra hard to find them first. + it's very unlikely your feature idea is a new one. + - type: textarea + id: freeform + attributes: + label: describe your actual problem + placeholder: 'I want to do ... I tried ... It does not work because ...' + validations: + required: true + - type: input + id: version + attributes: + label: pre-commit --version + placeholder: pre-commit x.x.x + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yaml new file mode 100644 index 000000000..a2d14826c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yaml @@ -0,0 +1,6 @@ +blank_issues_enabled: false +contact_links: +- name: documentation + url: https://pre-commit.com +- name: pre-commit.ci issues + url: https://github.com/pre-commit-ci/issues From 16869444cae5ebea1917a2442e37eff381c44c76 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 8 Feb 2023 11:21:50 -0500 Subject: [PATCH 759/967] git mv .github/ISSUE_TEMPLATE/config.{yaml,yml} --- .github/ISSUE_TEMPLATE/{config.yaml => config.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/ISSUE_TEMPLATE/{config.yaml => config.yml} (100%) diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/config.yaml rename to .github/ISSUE_TEMPLATE/config.yml From 4bd1677cda652a92c38a6051e7b8a1d76e36364b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 8 Feb 2023 11:23:43 -0500 Subject: [PATCH 760/967] do template links need about? --- .github/ISSUE_TEMPLATE/config.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index a2d14826c..4179f47f3 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,5 +2,7 @@ blank_issues_enabled: false contact_links: - name: documentation url: https://pre-commit.com + about: please check the docs first - name: pre-commit.ci issues url: https://github.com/pre-commit-ci/issues + about: please report issues about pre-commit.ci here From 4fdfb25a5245e63dd424f72ef7f66dfe49b2b53b Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Fri, 10 Feb 2023 16:18:43 -0600 Subject: [PATCH 761/967] test fail language inline --- tests/languages/fail_test.py | 14 ++++++++++++++ tests/repository_test.py | 24 ------------------------ 2 files changed, 14 insertions(+), 24 deletions(-) create mode 100644 tests/languages/fail_test.py diff --git a/tests/languages/fail_test.py b/tests/languages/fail_test.py new file mode 100644 index 000000000..7c74886fd --- /dev/null +++ b/tests/languages/fail_test.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from pre_commit.languages import fail +from testing.language_helpers import run_language + + +def test_fail_hooks(tmp_path): + ret = run_language( + tmp_path, + fail, + 'watch out for', + file_args=('bunnies',), + ) + assert ret == (1, b'watch out for\n\nbunnies\n') diff --git a/tests/repository_test.py b/tests/repository_test.py index 9e2f1e519..1a16e691f 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -330,30 +330,6 @@ def test_repository_state_compatibility(tempdir_factory, store, v): assert _hook_installed(hook) is True -def test_fail_hooks(store): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'fail', - 'name': 'fail', - 'language': 'fail', - 'entry': 'make sure to name changelogs as .rst!', - 'files': r'changelog/.*(? Date: Tue, 14 Feb 2023 02:25:01 +0000 Subject: [PATCH 762/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.991 → v1.0.0](https://github.com/pre-commit/mirrors-mypy/compare/v0.991...v1.0.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b7d7f1f0d..023f4f683 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.991 + rev: v1.0.0 hooks: - id: mypy additional_dependencies: [types-all] From 8db5aaf4f32f9ac3d4407f70478d0aa15c0d4680 Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Fri, 17 Feb 2023 21:30:46 -0600 Subject: [PATCH 763/967] future-proof dotnet build command see https://github.com/dotnet/sdk/issues/30624#issuecomment-1435457318 --- pre_commit/languages/dotnet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index 05d4ce32c..3db2679d3 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -61,7 +61,7 @@ def install_environment( helpers.assert_no_additional_deps('dotnet', additional_dependencies) envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) - build_dir = 'pre-commit-build' + build_dir = prefix.path('pre-commit-build') # Build & pack nupkg file helpers.run_setup_cmd( @@ -69,7 +69,7 @@ def install_environment( ( 'dotnet', 'pack', '--configuration', 'Release', - '--output', build_dir, + '--property', f'PackageOutputPath={build_dir}', ), ) From a2373d0a8198425785951cbd5f037d9815abb2ab Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Wed, 15 Feb 2023 20:50:19 -0600 Subject: [PATCH 764/967] test pygrep inline --- tests/languages/pygrep_test.py | 17 +++++++++++++ tests/repository_test.py | 46 ---------------------------------- 2 files changed, 17 insertions(+), 46 deletions(-) diff --git a/tests/languages/pygrep_test.py b/tests/languages/pygrep_test.py index 8420046c5..c6271c807 100644 --- a/tests/languages/pygrep_test.py +++ b/tests/languages/pygrep_test.py @@ -3,6 +3,7 @@ import pytest from pre_commit.languages import pygrep +from testing.language_helpers import run_language @pytest.fixture @@ -13,6 +14,9 @@ def some_files(tmpdir): tmpdir.join('f4').write_binary(b'foo\npattern\nbar\n') tmpdir.join('f5').write_binary(b'[INFO] hi\npattern\nbar') tmpdir.join('f6').write_binary(b"pattern\nbarwith'foo\n") + tmpdir.join('f7').write_binary(b"hello'hi\nworld\n") + tmpdir.join('f8').write_binary(b'foo\nbar\nbaz\n') + tmpdir.join('f9').write_binary(b'[WARN] hi\n') with tmpdir.as_cwd(): yield @@ -125,3 +129,16 @@ def test_multiline_multiline_flag_is_enabled(cap_out): out = cap_out.get() assert ret == 1 assert out == 'f1:1:foo\nbar\n' + + +def test_grep_hook_matching(some_files, tmp_path): + ret = run_language( + tmp_path, pygrep, 'ello', file_args=('f7', 'f8', 'f9'), + ) + assert ret == (1, b"f7:1:hello'hi\n") + + +@pytest.mark.parametrize('regex', ('nope', "foo'bar", r'^\[INFO\]')) +def test_grep_hook_not_matching(regex, some_files, tmp_path): + ret = run_language(tmp_path, pygrep, regex, file_args=('f7', 'f8', 'f9')) + assert ret == (0, b'') diff --git a/tests/repository_test.py b/tests/repository_test.py index 1a16e691f..332816d25 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -226,52 +226,6 @@ def test_output_isatty(tempdir_factory, store): ) -def _make_grep_repo(entry, store, args=()): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'grep-hook', - 'name': 'grep-hook', - 'language': 'pygrep', - 'entry': entry, - 'args': args, - 'types': ['text'], - }], - } - return _get_hook(config, store, 'grep-hook') - - -@pytest.fixture -def greppable_files(tmpdir): - with tmpdir.as_cwd(): - cmd_output_b('git', 'init', '.') - tmpdir.join('f1').write_binary(b"hello'hi\nworld\n") - tmpdir.join('f2').write_binary(b'foo\nbar\nbaz\n') - tmpdir.join('f3').write_binary(b'[WARN] hi\n') - yield tmpdir - - -def test_grep_hook_matching(greppable_files, store): - hook = _make_grep_repo('ello', store) - ret, out = _hook_run(hook, ('f1', 'f2', 'f3'), color=False) - assert ret == 1 - assert _norm_out(out) == b"f1:1:hello'hi\n" - - -def test_grep_hook_case_insensitive(greppable_files, store): - hook = _make_grep_repo('ELLO', store, args=['-i']) - ret, out = _hook_run(hook, ('f1', 'f2', 'f3'), color=False) - assert ret == 1 - assert _norm_out(out) == b"f1:1:hello'hi\n" - - -@pytest.mark.parametrize('regex', ('nope', "foo'bar", r'^\[INFO\]')) -def test_grep_hook_not_matching(regex, greppable_files, store): - hook = _make_grep_repo(regex, store) - ret, out = _hook_run(hook, ('f1', 'f2', 'f3'), color=False) - assert (ret, out) == (0, b'') - - def _norm_pwd(path): # Under windows bash's temp and windows temp is different. # This normalizes to the bash /tmp From d3883ce7f77f6cb88b622326de23c09cf8552cf6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Feb 2023 17:59:15 -0500 Subject: [PATCH 765/967] move languages.all and languages.helpers out of languages --- pre_commit/all_languages.py | 48 +++++++++ pre_commit/clientlib.py | 8 +- pre_commit/commands/run.py | 2 +- .../{languages/helpers.py => lang_base.py} | 45 ++++++++- pre_commit/languages/all.py | 99 ------------------- pre_commit/languages/conda.py | 14 +-- pre_commit/languages/coursier.py | 18 ++-- pre_commit/languages/dart.py | 20 ++-- pre_commit/languages/docker.py | 20 ++-- pre_commit/languages/docker_image.py | 14 +-- pre_commit/languages/dotnet.py | 20 ++-- pre_commit/languages/fail.py | 10 +- pre_commit/languages/golang.py | 16 +-- pre_commit/languages/lua.py | 18 ++-- pre_commit/languages/node.py | 18 ++-- pre_commit/languages/perl.py | 14 +-- pre_commit/languages/pygrep.py | 10 +- pre_commit/languages/python.py | 14 +-- pre_commit/languages/r.py | 12 +-- pre_commit/languages/ruby.py | 24 ++--- pre_commit/languages/rust.py | 12 +-- pre_commit/languages/script.py | 14 +-- pre_commit/languages/swift.py | 16 +-- pre_commit/languages/system.py | 12 +-- pre_commit/repository.py | 4 +- testing/language_helpers.py | 2 +- .../all_test.py => all_languages_test.py} | 2 +- .../helpers_test.py => lang_base_test.py} | 34 +++---- tests/languages/golang_test.py | 4 +- tests/repository_test.py | 12 +-- 30 files changed, 274 insertions(+), 282 deletions(-) create mode 100644 pre_commit/all_languages.py rename pre_commit/{languages/helpers.py => lang_base.py} (75%) delete mode 100644 pre_commit/languages/all.py rename tests/{languages/all_test.py => all_languages_test.py} (75%) rename tests/{languages/helpers_test.py => lang_base_test.py} (78%) diff --git a/pre_commit/all_languages.py b/pre_commit/all_languages.py new file mode 100644 index 000000000..2bed7067f --- /dev/null +++ b/pre_commit/all_languages.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from pre_commit.lang_base import Language +from pre_commit.languages import conda +from pre_commit.languages import coursier +from pre_commit.languages import dart +from pre_commit.languages import docker +from pre_commit.languages import docker_image +from pre_commit.languages import dotnet +from pre_commit.languages import fail +from pre_commit.languages import golang +from pre_commit.languages import lua +from pre_commit.languages import node +from pre_commit.languages import perl +from pre_commit.languages import pygrep +from pre_commit.languages import python +from pre_commit.languages import r +from pre_commit.languages import ruby +from pre_commit.languages import rust +from pre_commit.languages import script +from pre_commit.languages import swift +from pre_commit.languages import system + + +languages: dict[str, Language] = { + 'conda': conda, + 'coursier': coursier, + 'dart': dart, + 'docker': docker, + 'docker_image': docker_image, + 'dotnet': dotnet, + 'fail': fail, + 'golang': golang, + 'lua': lua, + 'node': node, + 'perl': perl, + 'pygrep': pygrep, + 'python': python, + 'r': r, + 'ruby': ruby, + 'rust': rust, + 'script': script, + 'swift': swift, + 'system': system, + # TODO: fully deprecate `python_venv` + 'python_venv': python, +} +language_names = sorted(languages) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index e191d3a00..9ff38c6a2 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -12,8 +12,8 @@ from identify.identify import ALL_TAGS import pre_commit.constants as C +from pre_commit.all_languages import language_names from pre_commit.errors import FatalError -from pre_commit.languages.all import all_languages from pre_commit.yaml import yaml_load logger = logging.getLogger('pre_commit') @@ -49,7 +49,7 @@ def check_min_version(version: str) -> None: cfgv.Required('id', cfgv.check_string), cfgv.Required('name', cfgv.check_string), cfgv.Required('entry', cfgv.check_string), - cfgv.Required('language', cfgv.check_one_of(all_languages)), + cfgv.Required('language', cfgv.check_one_of(language_names)), cfgv.Optional('alias', cfgv.check_string, ''), cfgv.Optional('files', check_string_regex, ''), @@ -281,8 +281,8 @@ def check(self, dct: dict[str, Any]) -> None: ) DEFAULT_LANGUAGE_VERSION = cfgv.Map( 'DefaultLanguageVersion', None, - cfgv.NoAdditionalKeys(all_languages), - *(cfgv.Optional(x, cfgv.check_string, C.DEFAULT) for x in all_languages), + cfgv.NoAdditionalKeys(language_names), + *(cfgv.Optional(x, cfgv.check_string, C.DEFAULT) for x in language_names), ) CONFIG_SCHEMA = cfgv.Map( 'Config', None, diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index a7eb4f45a..c9bc55b42 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -19,9 +19,9 @@ from pre_commit import color from pre_commit import git from pre_commit import output +from pre_commit.all_languages import languages from pre_commit.clientlib import load_config from pre_commit.hook import Hook -from pre_commit.languages.all import languages from pre_commit.repository import all_hooks from pre_commit.repository import install_hook_envs from pre_commit.staged_files_only import staged_files_only diff --git a/pre_commit/languages/helpers.py b/pre_commit/lang_base.py similarity index 75% rename from pre_commit/languages/helpers.py rename to pre_commit/lang_base.py index d1be409c8..6ba412f0e 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/lang_base.py @@ -7,8 +7,10 @@ import re import shlex from typing import Any +from typing import ContextManager from typing import Generator from typing import NoReturn +from typing import Protocol from typing import Sequence import pre_commit.constants as C @@ -22,6 +24,47 @@ SHIMS_RE = re.compile(r'[/\\]shims[/\\]') +class Language(Protocol): + # Use `None` for no installation / environment + @property + def ENVIRONMENT_DIR(self) -> str | None: ... + # return a value to replace `'default` for `language_version` + def get_default_version(self) -> str: ... + # return whether the environment is healthy (or should be rebuilt) + def health_check(self, prefix: Prefix, version: str) -> str | None: ... + + # install a repository for the given language and language_version + def install_environment( + self, + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], + ) -> None: + ... + + # modify the environment for hook execution + def in_env( + self, + prefix: Prefix, + version: str, + ) -> ContextManager[None]: + ... + + # execute a hook and return the exit code and output + def run_hook( + self, + prefix: Prefix, + entry: str, + args: Sequence[str], + file_args: Sequence[str], + *, + is_local: bool, + require_serial: bool, + color: bool, + ) -> tuple[int, bytes]: + ... + + def exe_exists(exe: str) -> bool: found = parse_shebang.find_executable(exe) if found is None: # exe exists @@ -45,7 +88,7 @@ def exe_exists(exe: str) -> bool: ) -def run_setup_cmd(prefix: Prefix, cmd: tuple[str, ...], **kwargs: Any) -> None: +def setup_cmd(prefix: Prefix, cmd: tuple[str, ...], **kwargs: Any) -> None: cmd_output_b(*cmd, cwd=prefix.prefix_dir, **kwargs) diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py deleted file mode 100644 index d952ae1ab..000000000 --- a/pre_commit/languages/all.py +++ /dev/null @@ -1,99 +0,0 @@ -from __future__ import annotations - -from typing import ContextManager -from typing import Protocol -from typing import Sequence - -from pre_commit.languages import conda -from pre_commit.languages import coursier -from pre_commit.languages import dart -from pre_commit.languages import docker -from pre_commit.languages import docker_image -from pre_commit.languages import dotnet -from pre_commit.languages import fail -from pre_commit.languages import golang -from pre_commit.languages import lua -from pre_commit.languages import node -from pre_commit.languages import perl -from pre_commit.languages import pygrep -from pre_commit.languages import python -from pre_commit.languages import r -from pre_commit.languages import ruby -from pre_commit.languages import rust -from pre_commit.languages import script -from pre_commit.languages import swift -from pre_commit.languages import system -from pre_commit.prefix import Prefix - - -class Language(Protocol): - # Use `None` for no installation / environment - @property - def ENVIRONMENT_DIR(self) -> str | None: ... - # return a value to replace `'default` for `language_version` - def get_default_version(self) -> str: ... - - # return whether the environment is healthy (or should be rebuilt) - def health_check( - self, - prefix: Prefix, - language_version: str, - ) -> str | None: - ... - - # install a repository for the given language and language_version - def install_environment( - self, - prefix: Prefix, - version: str, - additional_dependencies: Sequence[str], - ) -> None: - ... - - # modify the environment for hook execution - def in_env( - self, - prefix: Prefix, - version: str, - ) -> ContextManager[None]: - ... - - # execute a hook and return the exit code and output - def run_hook( - self, - prefix: Prefix, - entry: str, - args: Sequence[str], - file_args: Sequence[str], - *, - is_local: bool, - require_serial: bool, - color: bool, - ) -> tuple[int, bytes]: - ... - - -languages: dict[str, Language] = { - 'conda': conda, - 'coursier': coursier, - 'dart': dart, - 'docker': docker, - 'docker_image': docker_image, - 'dotnet': dotnet, - 'fail': fail, - 'golang': golang, - 'lua': lua, - 'node': node, - 'perl': perl, - 'pygrep': pygrep, - 'python': python, - 'r': r, - 'ruby': ruby, - 'rust': rust, - 'script': script, - 'swift': swift, - 'system': system, - # TODO: fully deprecate `python_venv` - 'python_venv': python, -} -all_languages = sorted(languages) diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index e2fb01969..05f1d2919 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -5,19 +5,19 @@ from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import SubstitutionT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'conda' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def get_env_patch(env: str) -> PatchesT: @@ -41,7 +41,7 @@ def get_env_patch(env: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -60,11 +60,11 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - helpers.assert_version_default('conda', version) + lang_base.assert_version_default('conda', version) conda_exe = _conda_exe() - env_dir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + env_dir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) cmd_output_b( conda_exe, 'env', 'create', '-p', env_dir, '--file', 'environment.yml', cwd=prefix.prefix_dir, diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index 60757588d..9c5fbfe24 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -5,19 +5,19 @@ from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var from pre_commit.errors import FatalError -from pre_commit.languages import helpers from pre_commit.parse_shebang import find_executable from pre_commit.prefix import Prefix ENVIRONMENT_DIR = 'coursier' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def install_environment( @@ -25,7 +25,7 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - helpers.assert_version_default('coursier', version) + lang_base.assert_version_default('coursier', version) # Support both possible executable names (either "cs" or "coursier") cs = find_executable('cs') or find_executable('coursier') @@ -35,12 +35,12 @@ def install_environment( 'executables in the application search path', ) - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) def _install(*opts: str) -> None: assert cs is not None - helpers.run_setup_cmd(prefix, (cs, 'fetch', *opts)) - helpers.run_setup_cmd(prefix, (cs, 'install', '--dir', envdir, *opts)) + lang_base.setup_cmd(prefix, (cs, 'fetch', *opts)) + lang_base.setup_cmd(prefix, (cs, 'install', '--dir', envdir, *opts)) with in_env(prefix, version): channel = prefix.path('.pre-commit-channel') @@ -71,6 +71,6 @@ def get_env_patch(target_dir: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py index e3c1c5855..e8539caa2 100644 --- a/pre_commit/languages/dart.py +++ b/pre_commit/languages/dart.py @@ -7,19 +7,19 @@ from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import win_exe from pre_commit.yaml import yaml_load ENVIRONMENT_DIR = 'dartenv' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def get_env_patch(venv: str) -> PatchesT: @@ -30,7 +30,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -40,9 +40,9 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - helpers.assert_version_default('dart', version) + lang_base.assert_version_default('dart', version) - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) bin_dir = os.path.join(envdir, 'bin') def _install_dir(prefix_p: Prefix, pub_cache: str) -> None: @@ -51,10 +51,10 @@ def _install_dir(prefix_p: Prefix, pub_cache: str) -> None: with open(prefix_p.path('pubspec.yaml')) as f: pubspec_contents = yaml_load(f) - helpers.run_setup_cmd(prefix_p, ('dart', 'pub', 'get'), env=dart_env) + lang_base.setup_cmd(prefix_p, ('dart', 'pub', 'get'), env=dart_env) for executable in pubspec_contents['executables']: - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix_p, ( 'dart', 'compile', 'exe', @@ -77,7 +77,7 @@ def _install_dir(prefix_p: Prefix, pub_cache: str) -> None: else: dep_cmd = (dep,) - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix, ('dart', 'pub', 'cache', 'add', *dep_cmd), env={**os.environ, 'PUB_CACHE': dep_tmp}, diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 2212c5ccb..8e53ca9e3 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -5,16 +5,16 @@ import os from typing import Sequence -from pre_commit.languages import helpers +from pre_commit import lang_base from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'docker' PRE_COMMIT_LABEL = 'PRE_COMMIT' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -in_env = helpers.no_env # no special environment for docker +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +in_env = lang_base.no_env # no special environment for docker def _is_in_docker() -> bool: @@ -84,16 +84,16 @@ def build_docker_image( cmd += ('--pull',) # This must come last for old versions of docker. See #477 cmd += ('.',) - helpers.run_setup_cmd(prefix, cmd) + lang_base.setup_cmd(prefix, cmd) def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: # pragma: win32 no cover - helpers.assert_version_default('docker', version) - helpers.assert_no_additional_deps('docker', additional_dependencies) + lang_base.assert_version_default('docker', version) + lang_base.assert_no_additional_deps('docker', additional_dependencies) - directory = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + directory = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) # Docker doesn't really have relevant disk environment, but pre-commit # still needs to cleanup its state files on failure @@ -135,10 +135,10 @@ def run_hook( # automated cleanup of docker images. build_docker_image(prefix, pull=False) - entry_exe, *cmd_rest = helpers.hook_cmd(entry, args) + entry_exe, *cmd_rest = lang_base.hook_cmd(entry, args) entry_tag = ('--entrypoint', entry_exe, docker_tag(prefix)) - return helpers.run_xargs( + return lang_base.run_xargs( (*docker_cmd(), *entry_tag, *cmd_rest), file_args, require_serial=require_serial, diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index 8e5f2c04c..26f006e4a 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -2,15 +2,15 @@ from typing import Sequence -from pre_commit.languages import helpers +from pre_commit import lang_base from pre_commit.languages.docker import docker_cmd from pre_commit.prefix import Prefix ENVIRONMENT_DIR = None -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -install_environment = helpers.no_install -in_env = helpers.no_env +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +install_environment = lang_base.no_install +in_env = lang_base.no_env def run_hook( @@ -23,8 +23,8 @@ def run_hook( require_serial: bool, color: bool, ) -> tuple[int, bytes]: # pragma: win32 no cover - cmd = docker_cmd() + helpers.hook_cmd(entry, args) - return helpers.run_xargs( + cmd = docker_cmd() + lang_base.hook_cmd(entry, args) + return lang_base.run_xargs( cmd, file_args, require_serial=require_serial, diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index 3db2679d3..e9568f222 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -9,18 +9,18 @@ from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix ENVIRONMENT_DIR = 'dotnetenv' BIN_DIR = 'bin' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def get_env_patch(venv: str) -> PatchesT: @@ -31,7 +31,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -57,14 +57,14 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - helpers.assert_version_default('dotnet', version) - helpers.assert_no_additional_deps('dotnet', additional_dependencies) + lang_base.assert_version_default('dotnet', version) + lang_base.assert_no_additional_deps('dotnet', additional_dependencies) - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) build_dir = prefix.path('pre-commit-build') # Build & pack nupkg file - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix, ( 'dotnet', 'pack', @@ -99,7 +99,7 @@ def install_environment( # Install to bin dir with _nuget_config_no_sources() as nuget_config: - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix, ( 'dotnet', 'tool', 'install', diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index 33df067e4..a8ec6a53d 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -2,14 +2,14 @@ from typing import Sequence -from pre_commit.languages import helpers +from pre_commit import lang_base from pre_commit.prefix import Prefix ENVIRONMENT_DIR = None -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -install_environment = helpers.no_install -in_env = helpers.no_env +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +install_environment = lang_base.no_install +in_env = lang_base.no_env def run_hook( diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 3c4b652fa..bea91e9bd 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -19,17 +19,17 @@ from typing import Sequence import pre_commit.constants as C +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output from pre_commit.util import rmtree ENVIRONMENT_DIR = 'golangenv' -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook _ARCH_ALIASES = { 'x86_64': 'amd64', @@ -60,7 +60,7 @@ def _open_archive(bio: IO[bytes]) -> ContextManager[ExtractAll]: @functools.lru_cache(maxsize=1) def get_default_version() -> str: - if helpers.exe_exists('go'): + if lang_base.exe_exists('go'): return 'system' else: return C.DEFAULT @@ -121,7 +121,7 @@ def _install_go(version: str, dest: str) -> None: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir, version)): yield @@ -131,7 +131,7 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - env_dir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + env_dir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) if version != 'system': _install_go(version, env_dir) @@ -149,9 +149,9 @@ def install_environment( os.path.join(env_dir, '.go', 'bin'), os.environ['PATH'], )) - helpers.run_setup_cmd(prefix, ('go', 'install', './...'), env=env) + lang_base.setup_cmd(prefix, ('go', 'install', './...'), env=env) for dependency in additional_dependencies: - helpers.run_setup_cmd(prefix, ('go', 'install', dependency), env=env) + lang_base.setup_cmd(prefix, ('go', 'install', dependency), env=env) # save some disk space -- we don't need this after installation pkgdir = os.path.join(env_dir, 'pkg') diff --git a/pre_commit/languages/lua.py b/pre_commit/languages/lua.py index ffc40b505..12d066140 100644 --- a/pre_commit/languages/lua.py +++ b/pre_commit/languages/lua.py @@ -6,17 +6,17 @@ from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output ENVIRONMENT_DIR = 'lua_env' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def _get_lua_version() -> str: # pragma: win32 no cover @@ -45,7 +45,7 @@ def get_env_patch(d: str) -> PatchesT: # pragma: win32 no cover @contextlib.contextmanager # pragma: win32 no cover def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -55,9 +55,9 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: # pragma: win32 no cover - helpers.assert_version_default('lua', version) + lang_base.assert_version_default('lua', version) - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with in_env(prefix, version): # luarocks doesn't bootstrap a tree prior to installing # so ensure the directory exists. @@ -66,10 +66,10 @@ def install_environment( # Older luarocks (e.g., 2.4.2) expect the rockspec as an arg for rockspec in prefix.star('.rockspec'): make_cmd = ('luarocks', '--tree', envdir, 'make', rockspec) - helpers.run_setup_cmd(prefix, make_cmd) + lang_base.setup_cmd(prefix, make_cmd) # luarocks can't install multiple packages at once # so install them individually. for dependency in additional_dependencies: cmd = ('luarocks', '--tree', envdir, 'install', dependency) - helpers.run_setup_cmd(prefix, cmd) + lang_base.setup_cmd(prefix, cmd) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 9688da359..66d613637 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -8,11 +8,11 @@ from typing import Sequence import pre_commit.constants as C +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.languages.python import bin_dir from pre_commit.prefix import Prefix from pre_commit.util import cmd_output @@ -20,7 +20,7 @@ from pre_commit.util import rmtree ENVIRONMENT_DIR = 'node_env' -run_hook = helpers.basic_run_hook +run_hook = lang_base.basic_run_hook @functools.lru_cache(maxsize=1) @@ -30,7 +30,7 @@ def get_default_version() -> str: return C.DEFAULT # if node is already installed, we can save a bunch of setup time by # using the installed version - elif all(helpers.exe_exists(exe) for exe in ('node', 'npm')): + elif all(lang_base.exe_exists(exe) for exe in ('node', 'npm')): return 'system' else: return C.DEFAULT @@ -60,13 +60,13 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield -def health_check(prefix: Prefix, language_version: str) -> str | None: - with in_env(prefix, language_version): +def health_check(prefix: Prefix, version: str) -> str | None: + with in_env(prefix, version): retcode, _, _ = cmd_output_b('node', '--version', check=False) if retcode != 0: # pragma: win32 no cover return f'`node --version` returned {retcode}' @@ -78,7 +78,7 @@ def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: assert prefix.exists('package.json') - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx?f=255&MSPPError=-2147217396#maxpath if sys.platform == 'win32': # pragma: no cover @@ -96,13 +96,13 @@ def install_environment( 'npm', 'install', '--dev', '--prod', '--ignore-prepublish', '--no-progress', '--no-save', ) - helpers.run_setup_cmd(prefix, local_install_cmd) + lang_base.setup_cmd(prefix, local_install_cmd) _, pkg, _ = cmd_output('npm', 'pack', cwd=prefix.prefix_dir) pkg = prefix.path(pkg.strip()) install = ('npm', 'install', '-g', pkg, *additional_dependencies) - helpers.run_setup_cmd(prefix, install) + lang_base.setup_cmd(prefix, install) # clean these up after installation if prefix.exists('node_modules'): # pragma: win32 no cover diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py index 2530c0ee1..2a7f16290 100644 --- a/pre_commit/languages/perl.py +++ b/pre_commit/languages/perl.py @@ -6,16 +6,16 @@ from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix ENVIRONMENT_DIR = 'perl_env' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def get_env_patch(venv: str) -> PatchesT: @@ -34,7 +34,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -42,9 +42,9 @@ def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: - helpers.assert_version_default('perl', version) + lang_base.assert_version_default('perl', version) with in_env(prefix, version): - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix, ('cpan', '-T', '.', *additional_dependencies), ) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index f0eb9a959..ec55560b0 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -7,16 +7,16 @@ from typing import Pattern from typing import Sequence +from pre_commit import lang_base from pre_commit import output -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.xargs import xargs ENVIRONMENT_DIR = None -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -install_environment = helpers.no_install -in_env = helpers.no_env +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +install_environment = lang_base.no_install +in_env = lang_base.no_env def _process_filename_by_line(pattern: Pattern[bytes], filename: str) -> int: diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index c373646bc..976674e2b 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -8,11 +8,11 @@ from typing import Sequence import pre_commit.constants as C +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.parse_shebang import find_executable from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError @@ -21,7 +21,7 @@ from pre_commit.util import win_exe ENVIRONMENT_DIR = 'py_env' -run_hook = helpers.basic_run_hook +run_hook = lang_base.basic_run_hook @functools.lru_cache(maxsize=None) @@ -153,13 +153,13 @@ def norm_version(version: str) -> str | None: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield -def health_check(prefix: Prefix, language_version: str) -> str | None: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) +def health_check(prefix: Prefix, version: str) -> str | None: + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) pyvenv_cfg = os.path.join(envdir, 'pyvenv.cfg') # created with "old" virtualenv @@ -202,7 +202,7 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) venv_cmd = [sys.executable, '-mvirtualenv', envdir] python = norm_version(version) if python is not None: @@ -211,4 +211,4 @@ def install_environment( cmd_output_b(*venv_cmd, cwd='/') with in_env(prefix, version): - helpers.run_setup_cmd(prefix, install_cmd) + lang_base.setup_cmd(prefix, install_cmd) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index e2383658a..138a26e1e 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -7,18 +7,18 @@ from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b from pre_commit.util import win_exe ENVIRONMENT_DIR = 'renv' RSCRIPT_OPTS = ('--no-save', '--no-restore', '--no-site-file', '--no-environ') -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check def get_env_patch(venv: str) -> PatchesT: @@ -30,7 +30,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -93,7 +93,7 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - env_dir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + env_dir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) os.makedirs(env_dir, exist_ok=True) shutil.copy(prefix.path('renv.lock'), env_dir) shutil.copytree(prefix.path('renv'), os.path.join(env_dir, 'renv')) @@ -166,7 +166,7 @@ def run_hook( color: bool, ) -> tuple[int, bytes]: cmd = _cmd_from_hook(prefix, entry, args, is_local=is_local) - return helpers.run_xargs( + return lang_base.run_xargs( cmd, file_args, require_serial=require_serial, diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 4416f7280..0ee0a857c 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -9,23 +9,23 @@ from typing import Sequence import pre_commit.constants as C +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError from pre_commit.util import resource_bytesio ENVIRONMENT_DIR = 'rbenv' -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook @functools.lru_cache(maxsize=1) def get_default_version() -> str: - if all(helpers.exe_exists(exe) for exe in ('ruby', 'gem')): + if all(lang_base.exe_exists(exe) for exe in ('ruby', 'gem')): return 'system' else: return C.DEFAULT @@ -68,7 +68,7 @@ def get_env_patch( @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir, version)): yield @@ -83,7 +83,7 @@ def _install_rbenv( prefix: Prefix, version: str, ) -> None: # pragma: win32 no cover - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) _extract_resource('rbenv.tar.gz', prefix.path('.')) shutil.move(prefix.path('rbenv'), envdir) @@ -100,10 +100,10 @@ def _install_ruby( version: str, ) -> None: # pragma: win32 no cover try: - helpers.run_setup_cmd(prefix, ('rbenv', 'download', version)) + lang_base.setup_cmd(prefix, ('rbenv', 'download', version)) except CalledProcessError: # pragma: no cover (usually find with download) # Failed to download from mirror for some reason, build it instead - helpers.run_setup_cmd(prefix, ('rbenv', 'install', version)) + lang_base.setup_cmd(prefix, ('rbenv', 'install', version)) def install_environment( @@ -114,17 +114,17 @@ def install_environment( with in_env(prefix, version): # Need to call this before installing so rbenv's directories # are set up - helpers.run_setup_cmd(prefix, ('rbenv', 'init', '-')) + lang_base.setup_cmd(prefix, ('rbenv', 'init', '-')) if version != C.DEFAULT: _install_ruby(prefix, version) # Need to call this after installing to set up the shims - helpers.run_setup_cmd(prefix, ('rbenv', 'rehash')) + lang_base.setup_cmd(prefix, ('rbenv', 'rehash')) with in_env(prefix, version): - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix, ('gem', 'build', *prefix.star('.gemspec')), ) - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix, ( 'gem', 'install', diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 391fd8657..e98e0d02d 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -11,19 +11,19 @@ from typing import Sequence import pre_commit.constants as C +from pre_commit import lang_base from pre_commit import parse_shebang from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b from pre_commit.util import make_executable from pre_commit.util import win_exe ENVIRONMENT_DIR = 'rustenv' -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook @functools.lru_cache(maxsize=1) @@ -63,7 +63,7 @@ def get_env_patch(target_dir: str, version: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir, version)): yield @@ -78,7 +78,7 @@ def _add_dependencies( crate = f'{name}@{spec or "*"}' crates.append(crate) - helpers.run_setup_cmd(prefix, ('cargo', 'add', *crates)) + lang_base.setup_cmd(prefix, ('cargo', 'add', *crates)) def install_rust_with_toolchain(toolchain: str) -> None: @@ -116,7 +116,7 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) # There are two cases where we might want to specify more dependencies: # as dependencies for the library being built, and as binary packages diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 08325f469..89a3ab2d6 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -2,14 +2,14 @@ from typing import Sequence -from pre_commit.languages import helpers +from pre_commit import lang_base from pre_commit.prefix import Prefix ENVIRONMENT_DIR = None -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -install_environment = helpers.no_install -in_env = helpers.no_env +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +install_environment = lang_base.no_install +in_env = lang_base.no_env def run_hook( @@ -22,9 +22,9 @@ def run_hook( require_serial: bool, color: bool, ) -> tuple[int, bytes]: - cmd = helpers.hook_cmd(entry, args) + cmd = lang_base.hook_cmd(entry, args) cmd = (prefix.path(cmd[0]), *cmd[1:]) - return helpers.run_xargs( + return lang_base.run_xargs( cmd, file_args, require_serial=require_serial, diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index c66ad5fb0..8250ab703 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -5,10 +5,10 @@ from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b @@ -16,9 +16,9 @@ BUILD_CONFIG = 'release' ENVIRONMENT_DIR = 'swift_env' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def get_env_patch(venv: str) -> PatchesT: # pragma: win32 no cover @@ -28,7 +28,7 @@ def get_env_patch(venv: str) -> PatchesT: # pragma: win32 no cover @contextlib.contextmanager # pragma: win32 no cover def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -36,9 +36,9 @@ def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: # pragma: win32 no cover - helpers.assert_version_default('swift', version) - helpers.assert_no_additional_deps('swift', additional_dependencies) - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + lang_base.assert_version_default('swift', version) + lang_base.assert_no_additional_deps('swift', additional_dependencies) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) # Build the swift package os.mkdir(envdir) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 204cad727..f6ad688fa 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -1,10 +1,10 @@ from __future__ import annotations -from pre_commit.languages import helpers +from pre_commit import lang_base ENVIRONMENT_DIR = None -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -install_environment = helpers.no_install -in_env = helpers.no_env -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +install_environment = lang_base.no_install +in_env = lang_base.no_env +run_hook = lang_base.basic_run_hook diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 308e80c70..5183df47a 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -8,13 +8,13 @@ from typing import Sequence import pre_commit.constants as C +from pre_commit.all_languages import languages from pre_commit.clientlib import load_manifest from pre_commit.clientlib import LOCAL from pre_commit.clientlib import META from pre_commit.clientlib import parse_version from pre_commit.hook import Hook -from pre_commit.languages.all import languages -from pre_commit.languages.helpers import environment_dir +from pre_commit.lang_base import environment_dir from pre_commit.prefix import Prefix from pre_commit.store import Store from pre_commit.util import clean_path_on_failure diff --git a/testing/language_helpers.py b/testing/language_helpers.py index 0964fbb44..5ab2af2a9 100644 --- a/testing/language_helpers.py +++ b/testing/language_helpers.py @@ -3,7 +3,7 @@ import os from typing import Sequence -from pre_commit.languages.all import Language +from pre_commit.lang_base import Language from pre_commit.prefix import Prefix diff --git a/tests/languages/all_test.py b/tests/all_languages_test.py similarity index 75% rename from tests/languages/all_test.py rename to tests/all_languages_test.py index 33b8925fb..98c912150 100644 --- a/tests/languages/all_test.py +++ b/tests/all_languages_test.py @@ -1,6 +1,6 @@ from __future__ import annotations -from pre_commit.languages.all import languages +from pre_commit.all_languages import languages def test_python_venv_is_an_alias_to_python(): diff --git a/tests/languages/helpers_test.py b/tests/lang_base_test.py similarity index 78% rename from tests/languages/helpers_test.py rename to tests/lang_base_test.py index c209e7e6d..89a64a1f2 100644 --- a/tests/languages/helpers_test.py +++ b/tests/lang_base_test.py @@ -8,8 +8,8 @@ import pytest import pre_commit.constants as C +from pre_commit import lang_base from pre_commit import parse_shebang -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError @@ -32,42 +32,42 @@ def fake_expanduser(pth): def test_exe_exists_does_not_exist(find_exe_mck, homedir_mck): find_exe_mck.return_value = None - assert helpers.exe_exists('ruby') is False + assert lang_base.exe_exists('ruby') is False def test_exe_exists_exists(find_exe_mck, homedir_mck): find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby') - assert helpers.exe_exists('ruby') is True + assert lang_base.exe_exists('ruby') is True def test_exe_exists_false_if_shim(find_exe_mck, homedir_mck): find_exe_mck.return_value = os.path.normpath('/foo/shims/ruby') - assert helpers.exe_exists('ruby') is False + assert lang_base.exe_exists('ruby') is False def test_exe_exists_false_if_homedir(find_exe_mck, homedir_mck): find_exe_mck.return_value = os.path.normpath('/home/me/somedir/ruby') - assert helpers.exe_exists('ruby') is False + assert lang_base.exe_exists('ruby') is False def test_exe_exists_commonpath_raises_ValueError(find_exe_mck, homedir_mck): find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby') with mock.patch.object(os.path, 'commonpath', side_effect=ValueError): - assert helpers.exe_exists('ruby') is True + assert lang_base.exe_exists('ruby') is True def test_exe_exists_true_when_homedir_is_slash(find_exe_mck): find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby') with mock.patch.object(os.path, 'expanduser', return_value=os.sep): - assert helpers.exe_exists('ruby') is True + assert lang_base.exe_exists('ruby') is True def test_basic_get_default_version(): - assert helpers.basic_get_default_version() == C.DEFAULT + assert lang_base.basic_get_default_version() == C.DEFAULT def test_basic_health_check(): - assert helpers.basic_health_check(Prefix('.'), 'default') is None + assert lang_base.basic_health_check(Prefix('.'), 'default') is None def test_failed_setup_command_does_not_unicode_error(): @@ -79,12 +79,12 @@ def test_failed_setup_command_does_not_unicode_error(): # an assertion that this does not raise `UnicodeError` with pytest.raises(CalledProcessError): - helpers.run_setup_cmd(Prefix('.'), (sys.executable, '-c', script)) + lang_base.setup_cmd(Prefix('.'), (sys.executable, '-c', script)) def test_assert_no_additional_deps(): with pytest.raises(AssertionError) as excinfo: - helpers.assert_no_additional_deps('lang', ['hmmm']) + lang_base.assert_no_additional_deps('lang', ['hmmm']) msg, = excinfo.value.args assert msg == ( 'for now, pre-commit does not support additional_dependencies for ' @@ -96,19 +96,19 @@ def test_assert_no_additional_deps(): def test_target_concurrency_normal(): with mock.patch.object(multiprocessing, 'cpu_count', return_value=123): with mock.patch.dict(os.environ, {}, clear=True): - assert helpers.target_concurrency() == 123 + assert lang_base.target_concurrency() == 123 def test_target_concurrency_testing_env_var(): with mock.patch.dict( os.environ, {'PRE_COMMIT_NO_CONCURRENCY': '1'}, clear=True, ): - assert helpers.target_concurrency() == 1 + assert lang_base.target_concurrency() == 1 def test_target_concurrency_on_travis(): with mock.patch.dict(os.environ, {'TRAVIS': '1'}, clear=True): - assert helpers.target_concurrency() == 2 + assert lang_base.target_concurrency() == 2 def test_target_concurrency_cpu_count_not_implemented(): @@ -116,17 +116,17 @@ def test_target_concurrency_cpu_count_not_implemented(): multiprocessing, 'cpu_count', side_effect=NotImplementedError, ): with mock.patch.dict(os.environ, {}, clear=True): - assert helpers.target_concurrency() == 1 + assert lang_base.target_concurrency() == 1 def test_shuffled_is_deterministic(): seq = [str(i) for i in range(10)] expected = ['4', '0', '5', '1', '8', '6', '2', '3', '7', '9'] - assert helpers._shuffled(seq) == expected + assert lang_base._shuffled(seq) == expected def test_xargs_require_serial_is_not_shuffled(): - ret, out = helpers.run_xargs( + ret, out = lang_base.run_xargs( ('echo',), [str(i) for i in range(10)], require_serial=True, color=False, diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py index f5f9985b8..ec5a87875 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -6,9 +6,9 @@ import re_assert import pre_commit.constants as C +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.languages import golang -from pre_commit.languages import helpers from pre_commit.store import _make_local_repo from testing.language_helpers import run_language @@ -18,7 +18,7 @@ @pytest.fixture def exe_exists_mck(): - with mock.patch.object(helpers, 'exe_exists') as mck: + with mock.patch.object(lang_base, 'exe_exists') as mck: yield mck diff --git a/tests/repository_test.py b/tests/repository_test.py index 332816d25..c04eb3796 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -10,12 +10,12 @@ import re_assert import pre_commit.constants as C +from pre_commit import lang_base +from pre_commit.all_languages import languages from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest from pre_commit.hook import Hook -from pre_commit.languages import helpers from pre_commit.languages import python -from pre_commit.languages.all import languages from pre_commit.prefix import Prefix from pre_commit.repository import _hook_installed from pre_commit.repository import all_hooks @@ -275,7 +275,7 @@ def test_repository_state_compatibility(tempdir_factory, store, v): config = make_config_from_repo(path) hook = _get_hook(config, store, 'foo') - envdir = helpers.environment_dir( + envdir = lang_base.environment_dir( hook.prefix, python.ENVIRONMENT_DIR, hook.language_version, @@ -327,7 +327,7 @@ class MyKeyboardInterrupt(KeyboardInterrupt): # raise as well. with pytest.raises(MyKeyboardInterrupt): with mock.patch.object( - helpers, 'run_setup_cmd', side_effect=MyKeyboardInterrupt, + lang_base, 'setup_cmd', side_effect=MyKeyboardInterrupt, ): with mock.patch.object( shutil, 'rmtree', side_effect=MyKeyboardInterrupt, @@ -336,7 +336,7 @@ class MyKeyboardInterrupt(KeyboardInterrupt): # Should have made an environment, however this environment is broken! hook, = hooks - envdir = helpers.environment_dir( + envdir = lang_base.environment_dir( hook.prefix, python.ENVIRONMENT_DIR, hook.language_version, @@ -359,7 +359,7 @@ def test_invalidated_virtualenv(tempdir_factory, store): hook = _get_hook(config, store, 'foo') # Simulate breaking of the virtualenv - envdir = helpers.environment_dir( + envdir = lang_base.environment_dir( hook.prefix, python.ENVIRONMENT_DIR, hook.language_version, From c3613b954a7155e6143b52cb3f3defcab82ba3ae Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Feb 2023 18:18:08 -0500 Subject: [PATCH 766/967] test things more directly to improve coverage --- tests/languages/python_test.py | 23 +++++++++++++++++++++++ tests/languages/script_test.py | 14 ++++++++++++++ tests/languages/system_test.py | 9 +++++++++ tests/repository_test.py | 16 ---------------- 4 files changed, 46 insertions(+), 16 deletions(-) create mode 100644 tests/languages/script_test.py create mode 100644 tests/languages/system_test.py diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 54fb98feb..8bb284eb9 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -12,6 +12,7 @@ from pre_commit.prefix import Prefix from pre_commit.util import make_executable from pre_commit.util import win_exe +from testing.language_helpers import run_language def test_read_pyvenv_cfg(tmpdir): @@ -210,3 +211,25 @@ def test_unhealthy_then_replaced(python_dir): os.replace(f'{py_exe}.tmp', py_exe) assert python.health_check(prefix, C.DEFAULT) is None + + +def test_language_versioned_python_hook(tmp_path): + setup_py = '''\ +from setuptools import setup +setup( + name='example', + py_modules=['mod'], + entry_points={'console_scripts': ['myexe=mod:main']}, +) +''' + tmp_path.joinpath('setup.py').write_text(setup_py) + tmp_path.joinpath('mod.py').write_text('def main(): print("ohai")') + + # we patch this to force virtualenv executing with `-p` since we can't + # reliably have multiple pythons available in CI + with mock.patch.object( + python, + '_sys_executable_matches', + return_value=False, + ): + assert run_language(tmp_path, python, 'myexe') == (0, b'ohai\n') diff --git a/tests/languages/script_test.py b/tests/languages/script_test.py new file mode 100644 index 000000000..a02f615a9 --- /dev/null +++ b/tests/languages/script_test.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from pre_commit.languages import script +from pre_commit.util import make_executable +from testing.language_helpers import run_language + + +def test_script_language(tmp_path): + exe = tmp_path.joinpath('main') + exe.write_text('#!/usr/bin/env bash\necho hello hello world\n') + make_executable(exe) + + expected = (0, b'hello hello world\n') + assert run_language(tmp_path, script, 'main') == expected diff --git a/tests/languages/system_test.py b/tests/languages/system_test.py new file mode 100644 index 000000000..dcd9cf1e0 --- /dev/null +++ b/tests/languages/system_test.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from pre_commit.languages import system +from testing.language_helpers import run_language + + +def test_system_language(tmp_path): + expected = (0, b'hello hello world\n') + assert run_language(tmp_path, system, 'echo hello hello world') == expected diff --git a/tests/repository_test.py b/tests/repository_test.py index 332816d25..e8b540708 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -143,22 +143,6 @@ def test_python_venv_deprecation(store, caplog): ) -def test_language_versioned_python_hook(tempdir_factory, store): - # we patch this force virtualenv executing with `-p` since we can't - # reliably have multiple pythons available in CI - with mock.patch.object( - python, - '_sys_executable_matches', - return_value=False, - ): - _test_hook_repo( - tempdir_factory, store, 'python3_hooks_repo', - 'python3-hook', - [os.devnull], - f'3\n[{os.devnull!r}]\nHello World\n'.encode(), - ) - - def test_system_hook_with_spaces(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'system_hook_with_spaces_repo', From 0cc2856883adc8910c522f4c8eb4ba2b397ebff0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 Feb 2023 02:06:17 +0000 Subject: [PATCH 767/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.0.0 → v1.0.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.0.0...v1.0.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 023f4f683..ad8ffba73 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.0.0 + rev: v1.0.1 hooks: - id: mypy additional_dependencies: [types-all] From 25b8ad752831dbbe9c5469760baffef16f4630f6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Feb 2023 21:32:32 -0500 Subject: [PATCH 768/967] improve unit test coverage of lang_base --- tests/lang_base_test.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/lang_base_test.py b/tests/lang_base_test.py index 89a64a1f2..a532b6a54 100644 --- a/tests/lang_base_test.py +++ b/tests/lang_base_test.py @@ -82,6 +82,21 @@ def test_failed_setup_command_does_not_unicode_error(): lang_base.setup_cmd(Prefix('.'), (sys.executable, '-c', script)) +def test_environment_dir(tmp_path): + ret = lang_base.environment_dir(Prefix(tmp_path), 'langenv', 'default') + assert ret == f'{tmp_path}{os.sep}langenv-default' + + +def test_assert_version_default(): + with pytest.raises(AssertionError) as excinfo: + lang_base.assert_version_default('lang', '1.2.3') + msg, = excinfo.value.args + assert msg == ( + 'for now, pre-commit requires system-installed lang -- ' + 'you selected `language_version: 1.2.3`' + ) + + def test_assert_no_additional_deps(): with pytest.raises(AssertionError) as excinfo: lang_base.assert_no_additional_deps('lang', ['hmmm']) @@ -93,6 +108,14 @@ def test_assert_no_additional_deps(): ) +def test_no_env_noop(tmp_path): + before = os.environ.copy() + with lang_base.no_env(Prefix(tmp_path), '1.2.3'): + inside = os.environ.copy() + after = os.environ.copy() + assert before == inside == after + + def test_target_concurrency_normal(): with mock.patch.object(multiprocessing, 'cpu_count', return_value=123): with mock.patch.dict(os.environ, {}, clear=True): @@ -133,3 +156,18 @@ def test_xargs_require_serial_is_not_shuffled(): ) assert ret == 0 assert out.strip() == b'0 1 2 3 4 5 6 7 8 9' + + +def test_basic_run_hook(tmp_path): + ret, out = lang_base.basic_run_hook( + Prefix(tmp_path), + 'echo hi', + ['hello'], + ['file', 'file', 'file'], + is_local=False, + require_serial=False, + color=False, + ) + assert ret == 0 + out = out.replace(b'\r\n', b'\n') + assert out == b'hi hello file file file\n' From 8d84a7a2702b074a8b46f5e38af28bd576291251 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Feb 2023 21:45:04 -0500 Subject: [PATCH 769/967] resources_bytesio is only used by ruby --- pre_commit/languages/ruby.py | 9 +++++++-- pre_commit/util.py | 5 ----- tests/languages/ruby_test.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 0ee0a857c..76631f253 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -2,10 +2,12 @@ import contextlib import functools +import importlib.resources import os.path import shutil import tarfile from typing import Generator +from typing import IO from typing import Sequence import pre_commit.constants as C @@ -16,13 +18,16 @@ from pre_commit.envcontext import Var from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError -from pre_commit.util import resource_bytesio ENVIRONMENT_DIR = 'rbenv' health_check = lang_base.basic_health_check run_hook = lang_base.basic_run_hook +def _resource_bytesio(filename: str) -> IO[bytes]: + return importlib.resources.open_binary('pre_commit.resources', filename) + + @functools.lru_cache(maxsize=1) def get_default_version() -> str: if all(lang_base.exe_exists(exe) for exe in ('ruby', 'gem')): @@ -74,7 +79,7 @@ def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: def _extract_resource(filename: str, dest: str) -> None: - with resource_bytesio(filename) as bio: + with _resource_bytesio(filename) as bio: with tarfile.open(fileobj=bio) as tf: tf.extractall(dest) diff --git a/pre_commit/util.py b/pre_commit/util.py index 8ea48446a..3d448e318 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -12,7 +12,6 @@ from typing import Any from typing import Callable from typing import Generator -from typing import IO from pre_commit import parse_shebang @@ -36,10 +35,6 @@ def clean_path_on_failure(path: str) -> Generator[None, None, None]: raise -def resource_bytesio(filename: str) -> IO[bytes]: - return importlib.resources.open_binary('pre_commit.resources', filename) - - def resource_text(filename: str) -> str: return importlib.resources.read_text('pre_commit.resources', filename) diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 9cfaad5d0..6397a4347 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -9,8 +9,8 @@ from pre_commit import parse_shebang from pre_commit.envcontext import envcontext from pre_commit.languages import ruby +from pre_commit.languages.ruby import _resource_bytesio from pre_commit.store import _make_local_repo -from pre_commit.util import resource_bytesio from testing.language_helpers import run_language from testing.util import cwd from testing.util import xfailif_windows @@ -40,7 +40,7 @@ def test_uses_system_if_both_gem_and_ruby_are_available(find_exe_mck): ('rbenv.tar.gz', 'ruby-build.tar.gz', 'ruby-download.tar.gz'), ) def test_archive_root_stat(filename): - with resource_bytesio(filename) as f: + with _resource_bytesio(filename) as f: with tarfile.open(fileobj=f) as tarf: root, _, _ = filename.partition('.') assert oct(tarf.getmember(root).mode) == '0o755' From d23990cc8b3b6040c0e5a7455ab7104cd60a5df4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Feb 2023 22:21:31 -0500 Subject: [PATCH 770/967] use run_language for repository_test --- testing/language_helpers.py | 6 ++++-- tests/repository_test.py | 31 +++++++++++++++---------------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/testing/language_helpers.py b/testing/language_helpers.py index 5ab2af2a9..ead8dae27 100644 --- a/testing/language_helpers.py +++ b/testing/language_helpers.py @@ -16,6 +16,8 @@ def run_language( version: str | None = None, deps: Sequence[str] = (), is_local: bool = False, + require_serial: bool = True, + color: bool = False, ) -> tuple[int, bytes]: prefix = Prefix(str(path)) version = version or language.get_default_version() @@ -31,8 +33,8 @@ def run_language( args, file_args, is_local=is_local, - require_serial=True, - color=False, + require_serial=require_serial, + color=color, ) out = out.replace(b'\r\n', b'\n') return ret, out diff --git a/tests/repository_test.py b/tests/repository_test.py index 1af73e3aa..9e5d9d62f 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -25,25 +25,24 @@ from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo from testing.fixtures import modify_manifest +from testing.language_helpers import run_language from testing.util import cwd from testing.util import get_resource_path -def _norm_out(b): - return b.replace(b'\r\n', b'\n') - - def _hook_run(hook, filenames, color): - with languages[hook.language].in_env(hook.prefix, hook.language_version): - return languages[hook.language].run_hook( - hook.prefix, - hook.entry, - hook.args, - filenames, - is_local=hook.src == 'local', - require_serial=hook.require_serial, - color=color, - ) + return run_language( + path=hook.prefix.prefix_dir, + language=languages[hook.language], + exe=hook.entry, + args=hook.args, + file_args=filenames, + version=hook.language_version, + deps=hook.additional_dependencies, + is_local=hook.src == 'local', + require_serial=hook.require_serial, + color=color, + ) def _get_hook_no_install(repo_config, store, hook_id): @@ -77,7 +76,7 @@ def _test_hook_repo( hook = _get_hook(config, store, hook_id) ret, out = _hook_run(hook, args, color=color) assert ret == expected_return_code - assert _norm_out(out) == expected + assert out == expected def test_python_hook(tempdir_factory, store): @@ -425,7 +424,7 @@ def test_local_python_repo(store, local_python_config): assert hook.language_version != C.DEFAULT ret, out = _hook_run(hook, ('filename',), color=False) assert ret == 0 - assert _norm_out(out) == b"['filename']\nHello World\n" + assert out == b"['filename']\nHello World\n" def test_default_language_version(store, local_python_config): From 9655158d938d0f49df0a0eedc5c0d166a45d591a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Feb 2023 17:00:05 -0500 Subject: [PATCH 771/967] test languages only when they are changed --- .github/actions/pre-test/action.yml | 31 ----------- .github/workflows/languages.yaml | 82 +++++++++++++++++++++++++++++ testing/languages | 79 +++++++++++++++++++++++++++ tox.ini | 4 +- 4 files changed, 163 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/languages.yaml create mode 100755 testing/languages diff --git a/.github/actions/pre-test/action.yml b/.github/actions/pre-test/action.yml index 42bbf00b5..9d1eb2de6 100644 --- a/.github/actions/pre-test/action.yml +++ b/.github/actions/pre-test/action.yml @@ -5,36 +5,5 @@ inputs: runs: using: composite steps: - - name: setup (windows) - shell: bash - if: runner.os == 'Windows' - run: | - set -x - - echo 'TEMP=C:\TEMP' >> "$GITHUB_ENV" - - echo "$CONDA\Scripts" >> "$GITHUB_PATH" - - echo 'C:\Strawberry\perl\bin' >> "$GITHUB_PATH" - echo 'C:\Strawberry\perl\site\bin' >> "$GITHUB_PATH" - echo 'C:\Strawberry\c\bin' >> "$GITHUB_PATH" - - testing/get-coursier.sh - testing/get-dart.sh - - name: setup (linux) - shell: bash - if: runner.os == 'Linux' - run: | - set -x - - sudo apt-get update - sudo apt-get install -y --no-install-recommends \ - lua5.3 \ - liblua5.3-dev \ - luarocks - - testing/get-coursier.sh - testing/get-dart.sh - testing/get-swift.sh - uses: asottile/workflows/.github/actions/latest-git@v1.4.0 if: inputs.env == 'py38' && runner.os == 'Linux' diff --git a/.github/workflows/languages.yaml b/.github/workflows/languages.yaml new file mode 100644 index 000000000..8bc8e712f --- /dev/null +++ b/.github/workflows/languages.yaml @@ -0,0 +1,82 @@ +name: languages + +on: + push: + branches: [main, test-me-*] + tags: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + vars: + runs-on: ubuntu-latest + outputs: + languages: ${{ steps.vars.outputs.languages }} + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-python@v4 + with: + python-version: 3.8 + - name: install deps + run: python -mpip install -e . -r requirements-dev.txt + - name: vars + run: testing/languages ${{ github.event_name == 'push' && '--all' || '' }} + id: vars + language: + needs: [vars] + runs-on: ${{ matrix.os }} + if: needs.vars.outputs.languages != '[]' + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON(needs.vars.outputs.languages) }} + steps: + - uses: asottile/workflows/.github/actions/fast-checkout@v1.4.0 + - uses: actions/setup-python@v4 + with: + python-version: 3.8 + + - run: echo "$CONDA\Scripts" >> "$GITHUB_PATH" + shell: bash + if: matrix.os == 'windows-latest' && matrix.language == 'conda' + - run: testing/get-coursier.sh + shell: bash + if: matrix.language == 'coursier' + - run: testing/get-dart.sh + shell: bash + if: matrix.language == 'dart' + - run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + lua5.3 \ + liblua5.3-dev \ + luarocks + if: matrix.os == 'ubuntu-latest' && matrix.language == 'lua' + - run: | + echo 'C:\Strawberry\perl\bin' >> "$GITHUB_PATH" + echo 'C:\Strawberry\perl\site\bin' >> "$GITHUB_PATH" + echo 'C:\Strawberry\c\bin' >> "$GITHUB_PATH" + shell: bash + if: matrix.os == 'windows-latest' && matrix.language == 'perl' + - run: testing/get-swift.sh + if: matrix.os == 'ubuntu-latest' && matrix.language == 'swift' + + - name: install deps + run: python -mpip install -e . -r requirements-dev.txt + - name: run tests + run: coverage run -m pytest tests/languages/${{ matrix.language }}_test.py + - name: check coverage + run: coverage report --include pre_commit/languages/${{ matrix.language }}.py,tests/languages/${{ matrix.language }}_test.py + collector: + needs: [language] + if: always() + runs-on: ubuntu-latest + steps: + - name: check for failures + if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') + run: echo job failed && exit 1 diff --git a/testing/languages b/testing/languages new file mode 100755 index 000000000..5e8fc9e4f --- /dev/null +++ b/testing/languages @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import concurrent.futures +import json +import os.path +import subprocess +import sys + +EXCLUDED = frozenset(( + ('windows-latest', 'docker'), + ('windows-latest', 'docker_image'), + ('windows-latest', 'lua'), + ('windows-latest', 'swift'), +)) + + +def _lang_files(lang: str) -> frozenset[str]: + prog = f'''\ +import json +import os.path +import sys + +import pre_commit.languages.{lang} +import tests.languages.{lang}_test + +modules = sorted( + os.path.relpath(v.__file__) + for k, v in sys.modules.items() + if k.startswith(('pre_commit.', 'tests.', 'testing.')) +) +print(json.dumps(modules)) +''' + out = json.loads(subprocess.check_output((sys.executable, '-c', prog))) + return frozenset(out) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument('--all', action='store_true') + args = parser.parse_args() + + langs = [ + os.path.splitext(fname)[0] + for fname in sorted(os.listdir('pre_commit/languages')) + if fname.endswith('.py') and fname != '__init__.py' + ] + + if not args.all: + with concurrent.futures.ThreadPoolExecutor(os.cpu_count()) as exe: + by_lang = { + lang: files + for lang, files in zip(langs, exe.map(_lang_files, langs)) + } + + diff_cmd = ('git', 'diff', '--name-only', 'origin/main...HEAD') + files = set(subprocess.check_output(diff_cmd).decode().splitlines()) + + langs = [ + lang + for lang, lang_files in by_lang.items() + if lang_files & files + ] + + matched = [ + {'os': os, 'language': lang} + for os in ('windows-latest', 'ubuntu-latest') + for lang in langs + if (os, lang) not in EXCLUDED + ] + + with open(os.environ['GITHUB_OUTPUT'], 'a') as f: + f.write(f'languages={json.dumps(matched)}\n') + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/tox.ini b/tox.ini index a44f93d48..602679a60 100644 --- a/tox.ini +++ b/tox.ini @@ -6,8 +6,8 @@ deps = -rrequirements-dev.txt passenv = * commands = coverage erase - coverage run -m pytest {posargs:tests} - coverage report + coverage run -m pytest {posargs:tests} --ignore=tests/languages + coverage report --omit=pre_commit/languages/*,tests/languages/* [testenv:pre-commit] skip_install = true From 08fa5ffc4353f0d9255281e4914cff2acc1c0859 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 21 Feb 2023 11:06:24 -0500 Subject: [PATCH 772/967] make a change to trigger the language tests --- pre_commit/lang_base.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pre_commit/lang_base.py b/pre_commit/lang_base.py index 6ba412f0e..9480c559f 100644 --- a/pre_commit/lang_base.py +++ b/pre_commit/lang_base.py @@ -43,12 +43,7 @@ def install_environment( ... # modify the environment for hook execution - def in_env( - self, - prefix: Prefix, - version: str, - ) -> ContextManager[None]: - ... + def in_env(self, prefix: Prefix, version: str) -> ContextManager[None]: ... # execute a hook and return the exit code and output def run_hook( From cddc9cff0f05a8d9e3ca126df03962574efe98e9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 21 Feb 2023 12:09:26 -0500 Subject: [PATCH 773/967] only treat exit code 1 as a successful diff --- pre_commit/staged_files_only.py | 23 ++++++++++----- tests/staged_files_only_test.py | 52 +++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 172fb20b1..881235656 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -7,6 +7,7 @@ from typing import Generator from pre_commit import git +from pre_commit.errors import FatalError from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b @@ -49,12 +50,16 @@ def _intent_to_add_cleared() -> Generator[None, None, None]: @contextlib.contextmanager def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: tree = cmd_output('git', 'write-tree')[1].strip() - retcode, diff_stdout_binary, _ = cmd_output_b( + diff_cmd = ( 'git', 'diff-index', '--ignore-submodules', '--binary', '--exit-code', '--no-color', '--no-ext-diff', tree, '--', - check=False, ) - if retcode and diff_stdout_binary.strip(): + retcode, diff_stdout, diff_stderr = cmd_output_b(*diff_cmd, check=False) + if retcode == 0: + # There weren't any staged files so we don't need to do anything + # special + yield + elif retcode == 1 and diff_stdout.strip(): patch_filename = f'patch{int(time.time())}-{os.getpid()}' patch_filename = os.path.join(patch_dir, patch_filename) logger.warning('Unstaged files detected.') @@ -62,7 +67,7 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: # Save the current unstaged changes as a patch os.makedirs(patch_dir, exist_ok=True) with open(patch_filename, 'wb') as patch_file: - patch_file.write(diff_stdout_binary) + patch_file.write(diff_stdout) # prevent recursive post-checkout hooks (#1418) no_checkout_env = dict(os.environ, _PRE_COMMIT_SKIP_POST_CHECKOUT='1') @@ -86,10 +91,12 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: _git_apply(patch_filename) logger.info(f'Restored changes from {patch_filename}.') - else: - # There weren't any staged files so we don't need to do anything - # special - yield + else: # pragma: win32 no cover + # some error occurred while requesting the diff + e = CalledProcessError(retcode, diff_cmd, b'', diff_stderr) + raise FatalError( + f'pre-commit failed to diff -- perhaps due to permissions?\n\n{e}', + ) @contextlib.contextmanager diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index a91f31519..50f146be4 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -1,12 +1,15 @@ from __future__ import annotations +import contextlib import itertools import os.path import shutil import pytest +import re_assert from pre_commit import git +from pre_commit.errors import FatalError from pre_commit.staged_files_only import staged_files_only from pre_commit.util import cmd_output from testing.auto_namedtuple import auto_namedtuple @@ -14,6 +17,7 @@ from testing.util import cwd from testing.util import get_resource_path from testing.util import git_commit +from testing.util import xfailif_windows FOO_CONTENTS = '\n'.join(('1', '2', '3', '4', '5', '6', '7', '8', '')) @@ -382,3 +386,51 @@ def test_intent_to_add(in_git_dir, patch_dir): with staged_files_only(patch_dir): assert_no_diff() assert git.intent_to_add_files() == ['foo'] + + +@contextlib.contextmanager +def _unreadable(f): + orig = os.stat(f).st_mode + os.chmod(f, 0o000) + try: + yield + finally: + os.chmod(f, orig) + + +@xfailif_windows # pragma: win32 no cover +def test_failed_diff_does_not_discard_changes(in_git_dir, patch_dir): + # stage 3 files + for i in range(3): + with open(str(i), 'w') as f: + f.write(str(i)) + cmd_output('git', 'add', '0', '1', '2') + + # modify all of their contents + for i in range(3): + with open(str(i), 'w') as f: + f.write('new contents') + + with _unreadable('1'): + with pytest.raises(FatalError) as excinfo: + with staged_files_only(patch_dir): + raise AssertionError('should have errored on enter') + + # the diff command failed to produce a diff of `1` + msg, = excinfo.value.args + re_assert.Matches( + r'^pre-commit failed to diff -- perhaps due to permissions\?\n\n' + r'command: .*\n' + r'return code: 128\n' + r'stdout: \(none\)\n' + r'stderr:\n' + r' error: open\("1"\): Permission denied\n' + r' fatal: cannot hash 1\n' + # TODO: not sure why there's weird whitespace here + r' $', + ).assert_matches(msg) + + # even though it errored, the unstaged changes should still be present + for i in range(3): + with open(str(i)) as f: + assert f.read() == 'new contents' From 4ded56efac790028557e8ad446937d00dff7f05d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 21 Feb 2023 12:42:09 -0500 Subject: [PATCH 774/967] fix trailing whitespace in CalledProcessError output --- pre_commit/util.py | 2 +- tests/staged_files_only_test.py | 4 +--- tests/util_test.py | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index 3d448e318..ea0d4f525 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -62,7 +62,7 @@ def __init__( def __bytes__(self) -> bytes: def _indent_or_none(part: bytes | None) -> bytes: if part: - return b'\n ' + part.replace(b'\n', b'\n ') + return b'\n ' + part.replace(b'\n', b'\n ').rstrip() else: return b' (none)' diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 50f146be4..58dbe5ac6 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -425,9 +425,7 @@ def test_failed_diff_does_not_discard_changes(in_git_dir, patch_dir): r'stdout: \(none\)\n' r'stderr:\n' r' error: open\("1"\): Permission denied\n' - r' fatal: cannot hash 1\n' - # TODO: not sure why there's weird whitespace here - r' $', + r' fatal: cannot hash 1$', ).assert_matches(msg) # even though it errored, the unstaged changes should still be present diff --git a/tests/util_test.py b/tests/util_test.py index 310f8f58e..5b2621138 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -16,7 +16,7 @@ def test_CalledProcessError_str(): - error = CalledProcessError(1, ('exe',), b'output', b'errors') + error = CalledProcessError(1, ('exe',), b'output\n', b'errors\n') assert str(error) == ( "command: ('exe',)\n" 'return code: 1\n' From a631abdabf0fcc2bb31f85ae33dfdefb958fe03a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 22 Feb 2023 20:31:14 -0500 Subject: [PATCH 775/967] remove sorting for repo key for additional_deps in other languages this order can matter (such as ruby) --- pre_commit/repository.py | 2 +- pre_commit/store.py | 2 +- tests/store_test.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 5183df47a..040f238f0 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -33,7 +33,7 @@ def _state_filename_v2(venv: str) -> str: def _state(additional_deps: Sequence[str]) -> object: - return {'additional_dependencies': sorted(additional_deps)} + return {'additional_dependencies': additional_deps} def _read_state(venv: str) -> object | None: diff --git a/pre_commit/store.py b/pre_commit/store.py index 6ddc7c481..487e3e798 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -125,7 +125,7 @@ def connect( @classmethod def db_repo_name(cls, repo: str, deps: Sequence[str]) -> str: if deps: - return f'{repo}:{",".join(sorted(deps))}' + return f'{repo}:{",".join(deps)}' else: return repo diff --git a/tests/store_test.py b/tests/store_test.py index 146eac416..eaab94000 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -180,7 +180,7 @@ def test_create_when_store_already_exists(store): def test_db_repo_name(store): assert store.db_repo_name('repo', ()) == 'repo' - assert store.db_repo_name('repo', ('b', 'a', 'c')) == 'repo:a,b,c' + assert store.db_repo_name('repo', ('b', 'a', 'c')) == 'repo:b,a,c' def test_local_resources_reflects_reality(): From 294590fd124484a786ba90423fa5d89536a6de98 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 22 Feb 2023 20:53:02 -0500 Subject: [PATCH 776/967] v3.1.0 --- CHANGELOG.md | 18 ++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0998da98b..8a4278120 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +3.1.0 - 2023-02-22 +================== + +### Fixes +- Fix `dotnet` for `.sln`-based hooks for dotnet>=7.0.200. + - #2763 PR by @m-rsha. +- Prevent stashing when `diff` fails to execute. + - #2774 PR by @asottile. + - #2773 issue by @strubbly. +- Dependencies are no longer sorted in repository key. + - #2776 PR by @asottile. + +### Updating +- Deprecate `language: python_venv`. Use `language: python` instead. + - #2746 PR by @asottile. + - #2734 issue by @asottile. + + 3.0.4 - 2023-02-03 ================== diff --git a/setup.cfg b/setup.cfg index 56b856cad..d1f649fe7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.0.4 +version = 3.1.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 2700a7d62241d7bea52d5305b5bca88ad7072919 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 27 Feb 2023 20:49:22 -0500 Subject: [PATCH 777/967] set RUSTUP_HOME when using a non-system rust --- pre_commit/languages/rust.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index e98e0d02d..af5f483d3 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -142,10 +142,15 @@ def install_environment( else: packages_to_install.add((package,)) - with in_env(prefix, version): + with contextlib.ExitStack() as ctx: + ctx.enter_context(in_env(prefix, version)) + if version != 'system': install_rust_with_toolchain(_rust_toolchain(version)) + tmpdir = ctx.enter_context(tempfile.TemporaryDirectory()) + ctx.enter_context(envcontext((('RUSTUP_HOME', tmpdir),))) + if len(lib_deps) > 0: _add_dependencies(prefix, lib_deps) From 2822de9aa6284f2de1c5ff8d0884b38bc553afa5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 27 Feb 2023 21:07:23 -0500 Subject: [PATCH 778/967] v3.1.1 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a4278120..cfcef4530 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +3.1.1 - 2023-02-27 +================== + +### Fixes +- Fix `rust` with `language_version` and a non-writable host `RUSTUP_HOME`. + - pre-commit-ci/issues#173 by @Swiftb0y. + - #2788 by @asottile. + 3.1.0 - 2023-02-22 ================== diff --git a/setup.cfg b/setup.cfg index d1f649fe7..507c0ad13 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.1.0 +version = 3.1.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 5ce4a549d3e0ee441698a13e431cf207bc3b611f Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Fri, 3 Mar 2023 20:16:09 -0600 Subject: [PATCH 779/967] prefer `sys.platform` over `os.name` when checking for windows OS --- pre_commit/languages/conda.py | 3 ++- pre_commit/languages/python.py | 4 ++-- pre_commit/util.py | 2 +- testing/util.py | 3 ++- tests/languages/python_test.py | 4 ++-- tests/parse_shebang_test.py | 2 +- tests/repository_test.py | 3 ++- tests/xargs_test.py | 2 +- 8 files changed, 13 insertions(+), 10 deletions(-) diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index 05f1d2919..41c355e77 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -2,6 +2,7 @@ import contextlib import os +import sys from typing import Generator from typing import Sequence @@ -26,7 +27,7 @@ def get_env_patch(env: str) -> PatchesT: # $CONDA_PREFIX/Scripts and $CONDA_PREFIX. Whereas the latter only # seems to be used for python.exe. path: SubstitutionT = (os.path.join(env, 'bin'), os.pathsep, Var('PATH')) - if os.name == 'nt': # pragma: no cover (platform specific) + if sys.platform == 'win32': # pragma: win32 cover path = (env, os.pathsep, *path) path = (os.path.join(env, 'Scripts'), os.pathsep, *path) path = (os.path.join(env, 'Library', 'bin'), os.pathsep, *path) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 976674e2b..3ef343608 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -48,7 +48,7 @@ def _read_pyvenv_cfg(filename: str) -> dict[str, str]: def bin_dir(venv: str) -> str: """On windows there's a different directory for the virtualenv""" - bin_part = 'Scripts' if os.name == 'nt' else 'bin' + bin_part = 'Scripts' if sys.platform == 'win32' else 'bin' return os.path.join(venv, bin_part) @@ -137,7 +137,7 @@ def norm_version(version: str) -> str | None: elif _sys_executable_matches(version): # virtualenv defaults to our exe return None - if os.name == 'nt': # pragma: no cover (windows) + if sys.platform == 'win32': # pragma: no cover (windows) version_exec = _find_by_py_launcher(version) if version_exec: return version_exec diff --git a/pre_commit/util.py b/pre_commit/util.py index ea0d4f525..4f8e8357d 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -119,7 +119,7 @@ def cmd_output(*cmd: str, **kwargs: Any) -> tuple[int, str, str | None]: return returncode, stdout, stderr -if os.name != 'nt': # pragma: win32 no cover +if sys.platform != 'win32': # pragma: win32 no cover from os import openpty import termios diff --git a/testing/util.py b/testing/util.py index 7c68d0eee..0fee28265 100644 --- a/testing/util.py +++ b/testing/util.py @@ -3,6 +3,7 @@ import contextlib import os.path import subprocess +import sys import pytest @@ -30,7 +31,7 @@ def cmd_output_mocked_pre_commit_home( return ret, out.replace('\r\n', '\n'), None -xfailif_windows = pytest.mark.xfail(os.name == 'nt', reason='windows') +xfailif_windows = pytest.mark.xfail(sys.platform == 'win32', reason='windows') def run_opts( diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 8bb284eb9..a4000b416 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -36,10 +36,10 @@ def test_read_pyvenv_cfg_non_utf8(tmpdir): def test_norm_version_expanduser(): home = os.path.expanduser('~') - if os.name == 'nt': # pragma: nt cover + if sys.platform == 'win32': # pragma: win32 cover path = r'~\python343' expected_path = fr'{home}\python343' - else: # pragma: nt no cover + else: # pragma: win32 no cover path = '~/.pyenv/versions/3.4.3/bin/python' expected_path = f'{home}/.pyenv/versions/3.4.3/bin/python' result = python.norm_version(path) diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index 2fcb29ee7..dd97ca5d8 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -94,7 +94,7 @@ def test_normexe_does_not_exist_sep(): assert excinfo.value.args == ('Executable `./i-dont-exist-lol` not found',) -@pytest.mark.xfail(os.name == 'nt', reason='posix only') +@pytest.mark.xfail(sys.platform == 'win32', reason='posix only') def test_normexe_not_executable(tmpdir): # pragma: win32 no cover tmpdir.join('exe').ensure() with tmpdir.as_cwd(), pytest.raises(OSError) as excinfo: diff --git a/tests/repository_test.py b/tests/repository_test.py index 9e5d9d62f..8fe6e02bb 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -2,6 +2,7 @@ import os.path import shutil +import sys from typing import Any from unittest import mock @@ -198,7 +199,7 @@ def test_intermixed_stdout_stderr(tempdir_factory, store): ) -@pytest.mark.xfail(os.name == 'nt', reason='ptys are posix-only') +@pytest.mark.xfail(sys.platform == 'win32', reason='ptys are posix-only') def test_output_isatty(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'stdout_stderr_repo', diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 0530e50d1..7c41f98cd 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -187,7 +187,7 @@ def test_xargs_propagate_kwargs_to_cmd(): assert b'Pre commit is awesome' in stdout -@pytest.mark.xfail(os.name == 'nt', reason='posix only') +@pytest.mark.xfail(sys.platform == 'win32', reason='posix only') def test_xargs_color_true_makes_tty(): retcode, out = xargs.xargs( (sys.executable, '-c', 'import sys; print(sys.stdout.isatty())'), From 0616c0abf75d45d2bd793ced4b3bddc42b478662 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 Mar 2023 02:52:32 +0000 Subject: [PATCH 780/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-autopep8: v2.0.1 → v2.0.2](https://github.com/pre-commit/mirrors-autopep8/compare/v2.0.1...v2.0.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ad8ffba73..0aa2e9ea2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v2.0.1 + rev: v2.0.2 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 From 63a180a935dc0096d23a65aa48b84498b57b8760 Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Sun, 5 Mar 2023 05:16:00 -0600 Subject: [PATCH 781/967] rewrite `args with spaces` test to not require python --- tests/repository_test.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/tests/repository_test.py b/tests/repository_test.py index 8fe6e02bb..a6c58bc7d 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1,6 +1,7 @@ from __future__ import annotations import os.path +import shlex import shutil import sys from typing import Any @@ -17,6 +18,7 @@ from pre_commit.clientlib import load_manifest from pre_commit.hook import Hook from pre_commit.languages import python +from pre_commit.languages import system from pre_commit.prefix import Prefix from pre_commit.repository import _hook_installed from pre_commit.repository import all_hooks @@ -99,22 +101,6 @@ def test_python_hook_default_version(tempdir_factory, store): test_python_hook(tempdir_factory, store) -def test_python_hook_args_with_spaces(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'python_hooks_repo', - 'foo', - [], - b"['i have spaces', 'and\"\\'quotes', '$and !this']\n" - b'Hello World\n', - config_kwargs={ - 'hooks': [{ - 'id': 'foo', - 'args': ['i have spaces', 'and"\'quotes', '$and !this'], - }], - }, - ) - - def test_python_hook_weird_setup_cfg(in_git_dir, tempdir_factory, store): in_git_dir.join('setup.cfg').write('[install]\ninstall_scripts=/usr/sbin') @@ -583,3 +569,14 @@ def test_non_installable_hook_error_for_additional_dependencies(store, caplog): 'using language `system` which does not install an environment. ' 'Perhaps you meant to use a specific language?' ) + + +def test_args_with_spaces_and_quotes(tmp_path): + ret = run_language( + tmp_path, system, + f"{shlex.quote(sys.executable)} -c 'import sys; print(sys.argv[1:])'", + ('i have spaces', 'and"\'quotes', '$and !this'), + ) + + expected = b"['i have spaces', 'and\"\\'quotes', '$and !this']\n" + assert ret == (0, expected) From 8ab9747b339df5bfbf0b7ebb7ebd1885ad6baabd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 9 Mar 2023 11:00:31 -0500 Subject: [PATCH 782/967] show 20 slowest durations in CI --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 602679a60..609c2fe18 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,7 @@ deps = -rrequirements-dev.txt passenv = * commands = coverage erase - coverage run -m pytest {posargs:tests} --ignore=tests/languages + coverage run -m pytest {posargs:tests} --ignore=tests/languages --durations=20 coverage report --omit=pre_commit/languages/*,tests/languages/* [testenv:pre-commit] From e3e17a1617b90c081e043db32cb046ed010f2310 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 11 Mar 2023 14:15:49 -0500 Subject: [PATCH 783/967] make --hook-type and stages match --- pre_commit/clientlib.py | 67 ++++++++++++++++++++++++++++---- pre_commit/commands/hook_impl.py | 2 +- pre_commit/constants.py | 13 ------- pre_commit/main.py | 8 +++- testing/util.py | 2 +- tests/clientlib_test.py | 48 +++++++++++++++++++++++ tests/commands/hook_impl_test.py | 4 +- tests/commands/run_test.py | 14 +++---- tests/main_test.py | 6 +++ tests/repository_test.py | 25 +++++++----- 10 files changed, 147 insertions(+), 42 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 9ff38c6a2..cb7778bb2 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -6,6 +6,7 @@ import shlex import sys from typing import Any +from typing import NamedTuple from typing import Sequence import cfgv @@ -20,6 +21,20 @@ check_string_regex = cfgv.check_and(cfgv.check_string, cfgv.check_regex) +HOOK_TYPES = ( + 'commit-msg', + 'post-checkout', + 'post-commit', + 'post-merge', + 'post-rewrite', + 'pre-commit', + 'pre-merge-commit', + 'pre-push', + 'prepare-commit-msg', +) +# `manual` is not invoked by any installed git hook. See #719 +STAGES = (*HOOK_TYPES, 'manual') + def check_type_tag(tag: str) -> None: if tag not in ALL_TAGS: @@ -43,6 +58,46 @@ def check_min_version(version: str) -> None: ) +_STAGES = { + 'commit': 'pre-commit', + 'merge-commit': 'pre-merge-commit', + 'push': 'pre-push', +} + + +def transform_stage(stage: str) -> str: + return _STAGES.get(stage, stage) + + +class StagesMigrationNoDefault(NamedTuple): + key: str + default: Sequence[str] + + def check(self, dct: dict[str, Any]) -> None: + if self.key not in dct: + return + + val = dct[self.key] + cfgv.check_array(cfgv.check_any)(val) + + val = [transform_stage(v) for v in val] + cfgv.check_array(cfgv.check_one_of(STAGES))(val) + + def apply_default(self, dct: dict[str, Any]) -> None: + if self.key not in dct: + return + dct[self.key] = [transform_stage(v) for v in dct[self.key]] + + def remove_default(self, dct: dict[str, Any]) -> None: + raise NotImplementedError + + +class StagesMigration(StagesMigrationNoDefault): + def apply_default(self, dct: dict[str, Any]) -> None: + dct.setdefault(self.key, self.default) + super().apply_default(dct) + + MANIFEST_HOOK_DICT = cfgv.Map( 'Hook', 'id', @@ -70,7 +125,7 @@ def check_min_version(version: str) -> None: cfgv.Optional('log_file', cfgv.check_string, ''), cfgv.Optional('minimum_pre_commit_version', cfgv.check_string, '0'), cfgv.Optional('require_serial', cfgv.check_bool, False), - cfgv.Optional('stages', cfgv.check_array(cfgv.check_one_of(C.STAGES)), []), + StagesMigration('stages', []), cfgv.Optional('verbose', cfgv.check_bool, False), ) MANIFEST_SCHEMA = cfgv.Array(MANIFEST_HOOK_DICT) @@ -241,7 +296,9 @@ def check(self, dct: dict[str, Any]) -> None: cfgv.OptionalNoDefault(item.key, item.check_fn) for item in MANIFEST_HOOK_DICT.items if item.key != 'id' + if item.key != 'stages' ), + StagesMigrationNoDefault('stages', []), OptionalSensibleRegexAtHook('files', cfgv.check_string), OptionalSensibleRegexAtHook('exclude', cfgv.check_string), ) @@ -290,17 +347,13 @@ def check(self, dct: dict[str, Any]) -> None: cfgv.RequiredRecurse('repos', cfgv.Array(CONFIG_REPO_DICT)), cfgv.Optional( 'default_install_hook_types', - cfgv.check_array(cfgv.check_one_of(C.HOOK_TYPES)), + cfgv.check_array(cfgv.check_one_of(HOOK_TYPES)), ['pre-commit'], ), cfgv.OptionalRecurse( 'default_language_version', DEFAULT_LANGUAGE_VERSION, {}, ), - cfgv.Optional( - 'default_stages', - cfgv.check_array(cfgv.check_one_of(C.STAGES)), - C.STAGES, - ), + StagesMigration('default_stages', STAGES), cfgv.Optional('files', check_string_regex, ''), cfgv.Optional('exclude', check_string_regex, '^$'), cfgv.Optional('fail_fast', cfgv.check_bool, False), diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index f5995e9ad..25d99c297 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -84,7 +84,7 @@ def _ns( ) -> argparse.Namespace: return argparse.Namespace( color=color, - hook_stage=hook_type.replace('pre-', ''), + hook_stage=hook_type, remote_branch=remote_branch, local_branch=local_branch, from_ref=from_ref, diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 3f03ceed9..79a9bb692 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -10,17 +10,4 @@ VERSION = importlib.metadata.version('pre_commit') -# `manual` is not invoked by any installed git hook. See #719 -STAGES = ( - 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', - 'post-commit', 'manual', 'post-checkout', 'push', 'post-merge', - 'post-rewrite', -) - -HOOK_TYPES = ( - 'pre-commit', 'pre-merge-commit', 'pre-push', 'prepare-commit-msg', - 'commit-msg', 'post-commit', 'post-checkout', 'post-merge', - 'post-rewrite', -) - DEFAULT = 'default' diff --git a/pre_commit/main.py b/pre_commit/main.py index 3915993ff..62d171e66 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -7,6 +7,7 @@ from typing import Sequence import pre_commit.constants as C +from pre_commit import clientlib from pre_commit import git from pre_commit.color import add_color_option from pre_commit.commands.autoupdate import autoupdate @@ -52,7 +53,7 @@ def _add_config_option(parser: argparse.ArgumentParser) -> None: def _add_hook_type_option(parser: argparse.ArgumentParser) -> None: parser.add_argument( '-t', '--hook-type', - choices=C.HOOK_TYPES, action='append', dest='hook_types', + choices=clientlib.HOOK_TYPES, action='append', dest='hook_types', ) @@ -73,7 +74,10 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: help='When hooks fail, run `git diff` directly afterward.', ) parser.add_argument( - '--hook-stage', choices=C.STAGES, default='commit', + '--hook-stage', + choices=clientlib.STAGES, + type=clientlib.transform_stage, + default='pre-commit', help='The stage during which the hook is fired. One of %(choices)s', ) parser.add_argument( diff --git a/testing/util.py b/testing/util.py index 0fee28265..8e3934cf2 100644 --- a/testing/util.py +++ b/testing/util.py @@ -46,7 +46,7 @@ def run_opts( to_ref='', remote_name='', remote_url='', - hook_stage='commit', + hook_stage='pre-commit', show_diff_on_failure=False, commit_msg_filename='', prepare_commit_message_source='', diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index efb2aa84a..568b2e974 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -12,6 +12,7 @@ from pre_commit.clientlib import CONFIG_REPO_DICT from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import DEFAULT_LANGUAGE_VERSION +from pre_commit.clientlib import MANIFEST_HOOK_DICT from pre_commit.clientlib import MANIFEST_SCHEMA from pre_commit.clientlib import META_HOOK_DICT from pre_commit.clientlib import OptionalSensibleRegexAtHook @@ -416,3 +417,50 @@ def test_warn_additional(schema): x for x in schema.items if isinstance(x, cfgv.WarnAdditionalKeys) ) assert allowed_keys == set(warn_additional.keys) + + +def test_stages_migration_for_default_stages(): + cfg = { + 'default_stages': ['commit-msg', 'push', 'commit', 'merge-commit'], + 'repos': [], + } + cfgv.validate(cfg, CONFIG_SCHEMA) + cfg = cfgv.apply_defaults(cfg, CONFIG_SCHEMA) + assert cfg['default_stages'] == [ + 'commit-msg', 'pre-push', 'pre-commit', 'pre-merge-commit', + ] + + +def test_manifest_stages_defaulting(): + dct = { + 'id': 'fake-hook', + 'name': 'fake-hook', + 'entry': 'fake-hook', + 'language': 'system', + 'stages': ['commit-msg', 'push', 'commit', 'merge-commit'], + } + cfgv.validate(dct, MANIFEST_HOOK_DICT) + dct = cfgv.apply_defaults(dct, MANIFEST_HOOK_DICT) + assert dct['stages'] == [ + 'commit-msg', 'pre-push', 'pre-commit', 'pre-merge-commit', + ] + + +def test_config_hook_stages_defaulting_missing(): + dct = {'id': 'fake-hook'} + cfgv.validate(dct, CONFIG_HOOK_DICT) + dct = cfgv.apply_defaults(dct, CONFIG_HOOK_DICT) + assert dct == {'id': 'fake-hook'} + + +def test_config_hook_stages_defaulting(): + dct = { + 'id': 'fake-hook', + 'stages': ['commit-msg', 'push', 'commit', 'merge-commit'], + } + cfgv.validate(dct, CONFIG_HOOK_DICT) + dct = cfgv.apply_defaults(dct, CONFIG_HOOK_DICT) + assert dct == { + 'id': 'fake-hook', + 'stages': ['commit-msg', 'pre-push', 'pre-commit', 'pre-merge-commit'], + } diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index aa321dabc..169e1414b 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -142,7 +142,7 @@ def test_check_args_length_prepare_commit_msg_error(): def test_run_ns_pre_commit(): ns = hook_impl._run_ns('pre-commit', True, (), b'') assert ns is not None - assert ns.hook_stage == 'commit' + assert ns.hook_stage == 'pre-commit' assert ns.color is True @@ -245,7 +245,7 @@ def test_run_ns_pre_push_updating_branch(push_example): ns = hook_impl._run_ns('pre-push', False, args, stdin) assert ns is not None - assert ns.hook_stage == 'push' + assert ns.hook_stage == 'pre-push' assert ns.color is False assert ns.remote_name == 'origin' assert ns.remote_url == src diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index f1085d9bb..885b78d61 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -354,13 +354,13 @@ def test_show_diff_on_failure( ({'hook': 'bash_hook'}, (b'Bash hook', b'Passed'), 0, True), ( {'hook': 'nope'}, - (b'No hook with id `nope` in stage `commit`',), + (b'No hook with id `nope` in stage `pre-commit`',), 1, True, ), ( - {'hook': 'nope', 'hook_stage': 'push'}, - (b'No hook with id `nope` in stage `push`',), + {'hook': 'nope', 'hook_stage': 'pre-push'}, + (b'No hook with id `nope` in stage `pre-push`',), 1, True, ), @@ -818,7 +818,7 @@ def test_stages(cap_out, store, repo_with_passing_hook): 'language': 'pygrep', 'stages': [stage], } - for i, stage in enumerate(('commit', 'push', 'manual'), 1) + for i, stage in enumerate(('pre-commit', 'pre-push', 'manual'), 1) ], } add_config_to_repo(repo_with_passing_hook, config) @@ -833,8 +833,8 @@ def _run_for_stage(stage): assert printed.count(b'hook ') == 1 return printed - assert _run_for_stage('commit').startswith(b'hook 1...') - assert _run_for_stage('push').startswith(b'hook 2...') + assert _run_for_stage('pre-commit').startswith(b'hook 1...') + assert _run_for_stage('pre-push').startswith(b'hook 2...') assert _run_for_stage('manual').startswith(b'hook 3...') @@ -1173,7 +1173,7 @@ def test_args_hook_only(cap_out, store, repo_with_passing_hook): ), 'language': 'system', 'files': r'\.py$', - 'stages': ['commit'], + 'stages': ['pre-commit'], }, { 'id': 'do_not_commit', diff --git a/tests/main_test.py b/tests/main_test.py index 511592622..945349fa4 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -216,3 +216,9 @@ def test_expected_fatal_error_no_git_repo(in_tmpdir, cap_out, mock_store_dir): 'Is it installed, and are you in a Git repository directory?' ) assert cap_out_lines[-1] == f'Check the log at {log_file}' + + +def test_hook_stage_migration(mock_store_dir): + with mock.patch.object(main, 'run') as mck: + main.main(('run', '--hook-stage', 'commit')) + assert mck.call_args[0][2].hook_stage == 'pre-commit' diff --git a/tests/repository_test.py b/tests/repository_test.py index a6c58bc7d..903574ce3 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -417,7 +417,7 @@ def test_local_python_repo(store, local_python_config): def test_default_language_version(store, local_python_config): config: dict[str, Any] = { 'default_language_version': {'python': 'fake'}, - 'default_stages': ['commit'], + 'default_stages': ['pre-commit'], 'repos': [local_python_config], } @@ -434,18 +434,18 @@ def test_default_language_version(store, local_python_config): def test_default_stages(store, local_python_config): config: dict[str, Any] = { 'default_language_version': {'python': C.DEFAULT}, - 'default_stages': ['commit'], + 'default_stages': ['pre-commit'], 'repos': [local_python_config], } # `stages` was not set, should default hook, = all_hooks(config, store) - assert hook.stages == ['commit'] + assert hook.stages == ['pre-commit'] # `stages` is set, should not default - config['repos'][0]['hooks'][0]['stages'] = ['push'] + config['repos'][0]['hooks'][0]['stages'] = ['pre-push'] hook, = all_hooks(config, store) - assert hook.stages == ['push'] + assert hook.stages == ['pre-push'] def test_hook_id_not_present(tempdir_factory, store, caplog): @@ -513,11 +513,18 @@ def test_manifest_hooks(tempdir_factory, store): name='Bash hook', pass_filenames=True, require_serial=False, - stages=( - 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', - 'post-commit', 'manual', 'post-checkout', 'push', 'post-merge', + stages=[ + 'commit-msg', + 'post-checkout', + 'post-commit', + 'post-merge', 'post-rewrite', - ), + 'pre-commit', + 'pre-merge-commit', + 'pre-push', + 'prepare-commit-msg', + 'manual', + ], types=['file'], types_or=[], verbose=False, From f39154f69f864457595b21f00e81f0e989d05ddf Mon Sep 17 00:00:00 2001 From: Marcelo Galigniana Date: Fri, 27 Jan 2023 16:18:06 -0300 Subject: [PATCH 784/967] Add pre-rebase hook support --- pre_commit/clientlib.py | 1 + pre_commit/commands/hook_impl.py | 17 ++++++++++ pre_commit/commands/run.py | 5 +++ pre_commit/main.py | 11 +++++++ testing/util.py | 4 +++ tests/commands/hook_impl_test.py | 25 +++++++++++++++ tests/commands/install_uninstall_test.py | 40 ++++++++++++++++++++++++ tests/commands/run_test.py | 10 ++++++ tests/repository_test.py | 1 + 9 files changed, 114 insertions(+) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index cb7778bb2..d0651cae2 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -30,6 +30,7 @@ 'pre-commit', 'pre-merge-commit', 'pre-push', + 'pre-rebase', 'prepare-commit-msg', ) # `manual` is not invoked by any installed git hook. See #719 diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index 25d99c297..dab2135d4 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -73,6 +73,8 @@ def _ns( local_branch: str | None = None, from_ref: str | None = None, to_ref: str | None = None, + pre_rebase_upstream: str | None = None, + pre_rebase_branch: str | None = None, remote_name: str | None = None, remote_url: str | None = None, commit_msg_filename: str | None = None, @@ -89,6 +91,8 @@ def _ns( local_branch=local_branch, from_ref=from_ref, to_ref=to_ref, + pre_rebase_upstream=pre_rebase_upstream, + pre_rebase_branch=pre_rebase_branch, remote_name=remote_name, remote_url=remote_url, commit_msg_filename=commit_msg_filename, @@ -185,6 +189,12 @@ def _check_args_length(hook_type: str, args: Sequence[str]) -> None: f'hook-impl for {hook_type} expected 1, 2, or 3 arguments ' f'but got {len(args)}: {args}', ) + elif hook_type == 'pre-rebase': + if len(args) < 1 or len(args) > 2: + raise SystemExit( + f'hook-impl for {hook_type} expected 1 or 2 arguments ' + f'but got {len(args)}: {args}', + ) elif hook_type in _EXPECTED_ARG_LENGTH_BY_HOOK: expected = _EXPECTED_ARG_LENGTH_BY_HOOK[hook_type] if len(args) != expected: @@ -231,6 +241,13 @@ def _run_ns( return _ns(hook_type, color, is_squash_merge=args[0]) elif hook_type == 'post-rewrite': return _ns(hook_type, color, rewrite_command=args[0]) + elif hook_type == 'pre-rebase' and len(args) == 1: + return _ns(hook_type, color, pre_rebase_upstream=args[0]) + elif hook_type == 'pre-rebase' and len(args) == 2: + return _ns( + hook_type, color, pre_rebase_upstream=args[0], + pre_rebase_branch=args[1], + ) else: raise AssertionError(f'unexpected hook type: {hook_type}') diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index c9bc55b42..c867799e8 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -254,6 +254,7 @@ def _all_filenames(args: argparse.Namespace) -> Collection[str]: # these hooks do not operate on files if args.hook_stage in { 'post-checkout', 'post-commit', 'post-merge', 'post-rewrite', + 'pre-rebase', }: return () elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}: @@ -389,6 +390,10 @@ def run( environ['PRE_COMMIT_FROM_REF'] = args.from_ref environ['PRE_COMMIT_TO_REF'] = args.to_ref + if args.pre_rebase_upstream and args.pre_rebase_branch: + environ['PRE_COMMIT_PRE_REBASE_UPSTREAM'] = args.pre_rebase_upstream + environ['PRE_COMMIT_PRE_REBASE_BRANCH'] = args.pre_rebase_branch + if ( args.remote_name and args.remote_url and args.remote_branch and args.local_branch diff --git a/pre_commit/main.py b/pre_commit/main.py index 62d171e66..9615c5e14 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -107,6 +107,17 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: 'now checked out.' ), ) + parser.add_argument( + '--pre-rebase-upstream', help=( + 'The upstream from which the series was forked.' + ), + ) + parser.add_argument( + '--pre-rebase-branch', help=( + 'The branch being rebased, and is not set when ' + 'rebasing the current branch.' + ), + ) parser.add_argument( '--commit-msg-filename', help='Filename to check when running during `commit-msg`', diff --git a/testing/util.py b/testing/util.py index 8e3934cf2..08d52cbc3 100644 --- a/testing/util.py +++ b/testing/util.py @@ -44,6 +44,8 @@ def run_opts( local_branch='', from_ref='', to_ref='', + pre_rebase_upstream='', + pre_rebase_branch='', remote_name='', remote_url='', hook_stage='pre-commit', @@ -67,6 +69,8 @@ def run_opts( local_branch=local_branch, from_ref=from_ref, to_ref=to_ref, + pre_rebase_upstream=pre_rebase_upstream, + pre_rebase_branch=pre_rebase_branch, remote_name=remote_name, remote_url=remote_url, hook_stage=hook_stage, diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index 169e1414b..d757e85c0 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -100,6 +100,8 @@ def call(*_, **__): ('commit-msg', ['.git/COMMIT_EDITMSG']), ('post-commit', []), ('post-merge', ['1']), + ('pre-rebase', ['main', 'topic']), + ('pre-rebase', ['main']), ('post-checkout', ['old_head', 'new_head', '1']), ('post-rewrite', ['amend']), # multiple choices for commit-editmsg @@ -139,6 +141,13 @@ def test_check_args_length_prepare_commit_msg_error(): ) +def test_check_args_length_pre_rebase_error(): + with pytest.raises(SystemExit) as excinfo: + hook_impl._check_args_length('pre-rebase', []) + msg, = excinfo.value.args + assert msg == 'hook-impl for pre-rebase expected 1 or 2 arguments but got 0: []' # noqa: E501 + + def test_run_ns_pre_commit(): ns = hook_impl._run_ns('pre-commit', True, (), b'') assert ns is not None @@ -146,6 +155,22 @@ def test_run_ns_pre_commit(): assert ns.color is True +def test_run_ns_pre_rebase(): + ns = hook_impl._run_ns('pre-rebase', True, ('main', 'topic'), b'') + assert ns is not None + assert ns.hook_stage == 'pre-rebase' + assert ns.color is True + assert ns.pre_rebase_upstream == 'main' + assert ns.pre_rebase_branch == 'topic' + + ns = hook_impl._run_ns('pre-rebase', True, ('main',), b'') + assert ns is not None + assert ns.hook_stage == 'pre-rebase' + assert ns.color is True + assert ns.pre_rebase_upstream == 'main' + assert ns.pre_rebase_branch is None + + def test_run_ns_commit_msg(): ns = hook_impl._run_ns('commit-msg', False, ('.git/COMMIT_MSG',), b'') assert ns is not None diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index a1ecda867..8b0d3ece4 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -810,6 +810,46 @@ def test_post_merge_integration(tempdir_factory, store): assert os.path.exists('post-merge.tmp') +def test_pre_rebase_integration(tempdir_factory, store): + path = git_dir(tempdir_factory) + config = { + 'repos': [ + { + 'repo': 'local', + 'hooks': [{ + 'id': 'pre-rebase', + 'name': 'Pre rebase', + 'entry': 'touch pre-rebase.tmp', + 'language': 'system', + 'always_run': True, + 'verbose': True, + 'stages': ['pre-rebase'], + }], + }, + ], + } + write_config(path, config) + with cwd(path): + install(C.CONFIG_FILE, store, hook_types=['pre-rebase']) + open('foo', 'a').close() + cmd_output('git', 'add', '.') + git_commit() + + cmd_output('git', 'checkout', '-b', 'branch') + open('bar', 'a').close() + cmd_output('git', 'add', '.') + git_commit() + + cmd_output('git', 'checkout', 'master') + open('baz', 'a').close() + cmd_output('git', 'add', '.') + git_commit() + + cmd_output('git', 'checkout', 'branch') + cmd_output('git', 'rebase', 'master', 'branch') + assert os.path.exists('pre-rebase.tmp') + + def test_post_rewrite_integration(tempdir_factory, store): path = git_dir(tempdir_factory) config = { diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 885b78d61..dd15b94c5 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -563,6 +563,16 @@ def test_merge_conflict_resolved(cap_out, store, in_merge_conflict): assert msg in printed +def test_rebase(cap_out, store, repo_with_passing_hook): + args = run_opts(pre_rebase_upstream='master', pre_rebase_branch='topic') + environ: MutableMapping[str, str] = {} + ret, printed = _do_run( + cap_out, store, repo_with_passing_hook, args, environ, + ) + assert environ['PRE_COMMIT_PRE_REBASE_UPSTREAM'] == 'master' + assert environ['PRE_COMMIT_PRE_REBASE_BRANCH'] == 'topic' + + @pytest.mark.parametrize( ('hooks', 'expected'), ( diff --git a/tests/repository_test.py b/tests/repository_test.py index 903574ce3..045656689 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -522,6 +522,7 @@ def test_manifest_hooks(tempdir_factory, store): 'pre-commit', 'pre-merge-commit', 'pre-push', + 'pre-rebase', 'prepare-commit-msg', 'manual', ], From d3c0a66d23b5cebc060f48278ddb43bcc3384dfc Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Sun, 12 Mar 2023 08:24:38 -0500 Subject: [PATCH 785/967] move slowest python-specific tests out of repository_test --- tests/languages/python_test.py | 51 ++++++++++++++++++++++++++++++++++ tests/repository_test.py | 29 ------------------- 2 files changed, 51 insertions(+), 29 deletions(-) diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index a4000b416..ab26e14e7 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -233,3 +233,54 @@ def test_language_versioned_python_hook(tmp_path): return_value=False, ): assert run_language(tmp_path, python, 'myexe') == (0, b'ohai\n') + + +def _make_hello_hello(tmp_path): + setup_py = '''\ +from setuptools import setup + +setup( + name='socks', + version='0.0.0', + py_modules=['socks'], + entry_points={'console_scripts': ['socks = socks:main']}, +) +''' + + main_py = '''\ +import sys + +def main(): + print(repr(sys.argv[1:])) + print('hello hello') + return 0 +''' + tmp_path.joinpath('setup.py').write_text(setup_py) + tmp_path.joinpath('socks.py').write_text(main_py) + + +def test_simple_python_hook(tmp_path): + _make_hello_hello(tmp_path) + + ret = run_language(tmp_path, python, 'socks', [os.devnull]) + assert ret == (0, f'[{os.devnull!r}]\nhello hello\n'.encode()) + + +def test_simple_python_hook_default_version(tmp_path): + # make sure that this continues to work for platforms where default + # language detection does not work + with mock.patch.object( + python, + 'get_default_version', + return_value=C.DEFAULT, + ): + test_simple_python_hook(tmp_path) + + +def test_python_hook_weird_setup_cfg(tmp_path): + _make_hello_hello(tmp_path) + setup_cfg = '[install]\ninstall_scripts=/usr/sbin' + tmp_path.joinpath('setup.cfg').write_text(setup_cfg) + + ret = run_language(tmp_path, python, 'socks', [os.devnull]) + assert ret == (0, f'[{os.devnull!r}]\nhello hello\n'.encode()) diff --git a/tests/repository_test.py b/tests/repository_test.py index 045656689..b8dde99b4 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -82,35 +82,6 @@ def _test_hook_repo( assert out == expected -def test_python_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'python_hooks_repo', - 'foo', [os.devnull], - f'[{os.devnull!r}]\nHello World\n'.encode(), - ) - - -def test_python_hook_default_version(tempdir_factory, store): - # make sure that this continues to work for platforms where default - # language detection does not work - with mock.patch.object( - python, - 'get_default_version', - return_value=C.DEFAULT, - ): - test_python_hook(tempdir_factory, store) - - -def test_python_hook_weird_setup_cfg(in_git_dir, tempdir_factory, store): - in_git_dir.join('setup.cfg').write('[install]\ninstall_scripts=/usr/sbin') - - _test_hook_repo( - tempdir_factory, store, 'python_hooks_repo', - 'foo', [os.devnull], - f'[{os.devnull!r}]\nHello World\n'.encode(), - ) - - def test_python_venv_deprecation(store, caplog): config = { 'repo': 'local', From 7a7772fcdae8694107b9ab19cc93ff5fdc690755 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 14 Mar 2023 03:19:10 +0000 Subject: [PATCH 786/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.0.1 → v1.1.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.0.1...v1.1.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0aa2e9ea2..cc96a7037 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.0.1 + rev: v1.1.1 hooks: - id: mypy additional_dependencies: [types-all] From a412e5492da8cdac6642b50cc3907db06edec109 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 17 Mar 2023 12:55:34 -0400 Subject: [PATCH 787/967] don't set CARGO_HOME in rust this adds a 270 MB registry cache in the output --- pre_commit/languages/rust.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index af5f483d3..a1f4dbe16 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -50,7 +50,6 @@ def _rust_toolchain(language_version: str) -> str: def get_env_patch(target_dir: str, version: str) -> PatchesT: return ( - ('CARGO_HOME', target_dir), ('PATH', (os.path.join(target_dir, 'bin'), os.pathsep, Var('PATH'))), # Only set RUSTUP_TOOLCHAIN if we don't want use the system's default # toolchain From df2cada973da6ee689cbc8e323caccf5c00df92c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 17 Mar 2023 14:26:34 -0400 Subject: [PATCH 788/967] v3.2.0 --- CHANGELOG.md | 15 +++++++++++++++ setup.cfg | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfcef4530..f2466e207 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +3.2.0 - 2023-03-17 +================== + +### Features +- Allow `pre-commit`, `pre-push`, and `pre-merge-commit` as `stages`. + - #2732 issue by @asottile. + - #2808 PR by @asottile. +- Add `pre-rebase` hook support. + - #2582 issue by @BrutalSimplicity. + - #2725 PR by @mgaligniana. + +### Fixes +- Remove bulky cargo cache from `language: rust` installs. + - #2820 PR by @asottile. + 3.1.1 - 2023-02-27 ================== diff --git a/setup.cfg b/setup.cfg index 507c0ad13..5b3d1560e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.1.1 +version = 3.2.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From ee71a9345ce96a78e011c9635a61abc332e38961 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 25 Mar 2023 13:06:22 -0400 Subject: [PATCH 789/967] set CARGO_HOME while executing rustup --- pre_commit/languages/rust.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index a1f4dbe16..7eec0e7d6 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -80,9 +80,9 @@ def _add_dependencies( lang_base.setup_cmd(prefix, ('cargo', 'add', *crates)) -def install_rust_with_toolchain(toolchain: str) -> None: +def install_rust_with_toolchain(toolchain: str, envdir: str) -> None: with tempfile.TemporaryDirectory() as rustup_dir: - with envcontext((('RUSTUP_HOME', rustup_dir),)): + with envcontext((('CARGO_HOME', envdir), ('RUSTUP_HOME', rustup_dir))): # acquire `rustup` if not present if parse_shebang.find_executable('rustup') is None: # We did not detect rustup and need to download it first. @@ -145,7 +145,7 @@ def install_environment( ctx.enter_context(in_env(prefix, version)) if version != 'system': - install_rust_with_toolchain(_rust_toolchain(version)) + install_rust_with_toolchain(_rust_toolchain(version), envdir) tmpdir = ctx.enter_context(tempfile.TemporaryDirectory()) ctx.enter_context(envcontext((('RUSTUP_HOME', tmpdir),))) From bb49560dc99a65608c8f9161dd71467af163c0d1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 25 Mar 2023 14:02:57 -0400 Subject: [PATCH 790/967] v3.2.1 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2466e207..dfb8f804f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +3.2.1 - 2023-03-25 +================== + +### Fixes +- Fix `language_version` for `language: rust` without global `rustup`. + - #2823 issue by @daschuer. + - #2827 PR by @asottile. + 3.2.0 - 2023-03-17 ================== diff --git a/setup.cfg b/setup.cfg index 5b3d1560e..350fe237a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.2.0 +version = 3.2.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 84f040f58a5710c2a9d6530f9d1e033657665f20 Mon Sep 17 00:00:00 2001 From: Eric DeLabar Date: Mon, 3 Apr 2023 15:50:55 -0400 Subject: [PATCH 791/967] fix #2235 --- pre_commit/languages/swift.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 8250ab703..f16bb0451 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -44,7 +44,7 @@ def install_environment( os.mkdir(envdir) cmd_output_b( 'swift', 'build', - '-C', prefix.prefix_dir, + '--package-path', prefix.prefix_dir, '-c', BUILD_CONFIG, '--build-path', os.path.join(envdir, BUILD_DIR), ) From 5027592625f8df286dea831e84e7bf83021b7c1b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 3 Apr 2023 16:31:09 -0400 Subject: [PATCH 792/967] v3.2.2 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfb8f804f..efd96c796 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +3.2.2 - 2023-04-03 +================== + +### Fixes +- Fix support for swift >= 5.8. + - #2836 PR by @edelabar. + - #2835 issue by @kgrobelny-intive. + 3.2.1 - 2023-03-25 ================== diff --git a/setup.cfg b/setup.cfg index 350fe237a..89e8e4ada 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.2.1 +version = 3.2.2 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From f5a716f1b1e2805444c199da7fbe24380100930c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 11 Apr 2023 02:57:43 +0000 Subject: [PATCH 793/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.1.1 → v1.2.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.1.1...v1.2.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cc96a7037..47d2630a4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.1.1 + rev: v1.2.0 hooks: - id: mypy additional_dependencies: [types-all] From cfcb88364e29a8855998502fed425c33d18c1252 Mon Sep 17 00:00:00 2001 From: Jamie Alessio Date: Tue, 18 Apr 2023 10:58:57 -0700 Subject: [PATCH 794/967] Upgrade to ruby-build v20230330 --- pre_commit/resources/ruby-build.tar.gz | Bin 76466 -> 75808 bytes testing/make-archives | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz index b6eacf59ba31c87032e76c2d1f39a87bb2b52489..19d467fdd2867742cdefe402a78489481ce9abba 100644 GIT binary patch literal 75808 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 aVnW&LI5^+h|Iiv<8JMAUgtgp` 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%0AtVc+vZ{$*a=M$ni`+p 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 Date: Tue, 25 Apr 2023 03:20:55 +0000 Subject: [PATCH 795/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.3.1 → v3.3.2](https://github.com/asottile/pyupgrade/compare/v3.3.1...v3.3.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 47d2630a4..ffd305878 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 + rev: v3.3.2 hooks: - id: pyupgrade args: [--py38-plus] From 4727922b9316703d97fb12319f9f459c47f88cc5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Apr 2023 13:29:00 -0400 Subject: [PATCH 796/967] use blobless clone for faster autoupdate --- pre_commit/commands/autoupdate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 7ed6e7761..a43d7dd95 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -50,7 +50,12 @@ def update(self, tags_only: bool, freeze: bool) -> RevInfo: with tempfile.TemporaryDirectory() as tmp: git.init_repo(tmp, self.repo) cmd_output_b( - *git_cmd, 'fetch', 'origin', 'HEAD', '--tags', + *git_cmd, 'config', 'extensions.partialClone', 'true', + cwd=tmp, + ) + cmd_output_b( + *git_cmd, 'fetch', 'origin', 'HEAD', + '--quiet', '--filter=blob:none', '--tags', cwd=tmp, ) From e885f2e76ed09c178a7e16a235b76ee4f6e765f6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Apr 2023 15:11:14 -0400 Subject: [PATCH 797/967] use -C for git commands in autoupdate --- pre_commit/commands/autoupdate.py | 35 +++++++++++-------------------- 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index a43d7dd95..347599f63 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -34,44 +34,33 @@ def from_config(cls, config: dict[str, Any]) -> RevInfo: return cls(config['repo'], config['rev'], None) def update(self, tags_only: bool, freeze: bool) -> RevInfo: - git_cmd = ('git', *git.NO_FS_MONITOR) + with tempfile.TemporaryDirectory() as tmp: + _git = ('git', *git.NO_FS_MONITOR, '-C', tmp) - if tags_only: - tag_cmd = ( - *git_cmd, 'describe', - 'FETCH_HEAD', '--tags', '--abbrev=0', - ) - else: - tag_cmd = ( - *git_cmd, 'describe', - 'FETCH_HEAD', '--tags', '--exact', - ) + if tags_only: + tag_opt = '--abbrev=0' + else: + tag_opt = '--exact' + tag_cmd = (*_git, 'describe', 'FETCH_HEAD', '--tags', tag_opt) - with tempfile.TemporaryDirectory() as tmp: git.init_repo(tmp, self.repo) + cmd_output_b(*_git, 'config', 'extensions.partialClone', 'true') cmd_output_b( - *git_cmd, 'config', 'extensions.partialClone', 'true', - cwd=tmp, - ) - cmd_output_b( - *git_cmd, 'fetch', 'origin', 'HEAD', + *_git, 'fetch', 'origin', 'HEAD', '--quiet', '--filter=blob:none', '--tags', - cwd=tmp, ) try: - rev = cmd_output(*tag_cmd, cwd=tmp)[1].strip() + rev = cmd_output(*tag_cmd)[1].strip() except CalledProcessError: - cmd = (*git_cmd, 'rev-parse', 'FETCH_HEAD') - rev = cmd_output(*cmd, cwd=tmp)[1].strip() + rev = cmd_output(*_git, 'rev-parse', 'FETCH_HEAD')[1].strip() else: if tags_only: rev = git.get_best_candidate_tag(rev, tmp) frozen = None if freeze: - exact_rev_cmd = (*git_cmd, 'rev-parse', rev) - exact = cmd_output(*exact_rev_cmd, cwd=tmp)[1].strip() + exact = cmd_output(*_git, 'rev-parse', rev)[1].strip() if exact != rev: rev, frozen = exact, rev return self._replace(rev=rev, frozen=frozen) From 4f045cbc21fd3113c50fc9592666908692b1d24e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Apr 2023 15:19:20 -0400 Subject: [PATCH 798/967] perform autoupdate without Store contention --- pre_commit/commands/autoupdate.py | 34 ++++++----- pre_commit/main.py | 2 +- tests/commands/autoupdate_test.py | 96 +++++++++++++++---------------- tests/commands/gc_test.py | 5 +- 4 files changed, 71 insertions(+), 66 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 347599f63..71e5c99b8 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -16,7 +16,6 @@ from pre_commit.clientlib import LOCAL from pre_commit.clientlib import META from pre_commit.commands.migrate_config import migrate_config -from pre_commit.store import Store from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b @@ -27,11 +26,12 @@ class RevInfo(NamedTuple): repo: str rev: str - frozen: str | None + frozen: str | None = None + hook_ids: frozenset[str] = frozenset() @classmethod def from_config(cls, config: dict[str, Any]) -> RevInfo: - return cls(config['repo'], config['rev'], None) + return cls(config['repo'], config['rev']) def update(self, tags_only: bool, freeze: bool) -> RevInfo: with tempfile.TemporaryDirectory() as tmp: @@ -63,7 +63,19 @@ def update(self, tags_only: bool, freeze: bool) -> RevInfo: exact = cmd_output(*_git, 'rev-parse', rev)[1].strip() if exact != rev: rev, frozen = exact, rev - return self._replace(rev=rev, frozen=frozen) + + try: + cmd_output(*_git, 'checkout', rev, '--', C.MANIFEST_FILE) + except CalledProcessError: + pass # this will be caught by manifest validating code + try: + manifest = load_manifest(os.path.join(tmp, C.MANIFEST_FILE)) + except InvalidManifestError as e: + raise RepositoryCannotBeUpdatedError(str(e)) + else: + hook_ids = frozenset(hook['id'] for hook in manifest) + + return self._replace(rev=rev, frozen=frozen, hook_ids=hook_ids) class RepositoryCannotBeUpdatedError(RuntimeError): @@ -73,17 +85,10 @@ class RepositoryCannotBeUpdatedError(RuntimeError): def _check_hooks_still_exist_at_rev( repo_config: dict[str, Any], info: RevInfo, - store: Store, ) -> None: - try: - path = store.clone(repo_config['repo'], info.rev) - manifest = load_manifest(os.path.join(path, C.MANIFEST_FILE)) - except InvalidManifestError as e: - raise RepositoryCannotBeUpdatedError(str(e)) - # See if any of our hooks were deleted with the new commits hooks = {hook['id'] for hook in repo_config['hooks']} - hooks_missing = hooks - {hook['id'] for hook in manifest} + hooks_missing = hooks - info.hook_ids if hooks_missing: raise RepositoryCannotBeUpdatedError( f'Cannot update because the update target is missing these ' @@ -139,7 +144,6 @@ def _write_new_config(path: str, rev_infos: list[RevInfo | None]) -> None: def autoupdate( config_file: str, - store: Store, tags_only: bool, freeze: bool, repos: Sequence[str] = (), @@ -161,9 +165,9 @@ def autoupdate( continue output.write(f'Updating {info.repo} ... ') - new_info = info.update(tags_only=tags_only, freeze=freeze) try: - _check_hooks_still_exist_at_rev(repo_config, new_info, store) + new_info = info.update(tags_only=tags_only, freeze=freeze) + _check_hooks_still_exist_at_rev(repo_config, new_info) except RepositoryCannotBeUpdatedError as error: output.write_line(error.args[0]) rev_infos.append(None) diff --git a/pre_commit/main.py b/pre_commit/main.py index 9615c5e14..402bc2e56 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -368,7 +368,7 @@ def _add_cmd(name: str, *, help: str) -> argparse.ArgumentParser: if args.command == 'autoupdate': return autoupdate( - args.config, store, + args.config, tags_only=not args.bleeding_edge, freeze=args.freeze, repos=args.repos, diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 4bcb5d82a..71bd04446 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -67,7 +67,7 @@ def test_rev_info_from_config(): def test_rev_info_update_up_to_date_repo(up_to_date): config = make_config_from_repo(up_to_date) - info = RevInfo.from_config(config) + info = RevInfo.from_config(config)._replace(hook_ids=frozenset(('foo',))) new_info = info.update(tags_only=False, freeze=False) assert info == new_info @@ -139,7 +139,7 @@ def test_rev_info_update_does_not_freeze_if_already_sha(out_of_date): assert new_info.frozen is None -def test_autoupdate_up_to_date_repo(up_to_date, tmpdir, store): +def test_autoupdate_up_to_date_repo(up_to_date, tmpdir): contents = ( f'repos:\n' f'- repo: {up_to_date}\n' @@ -150,11 +150,11 @@ def test_autoupdate_up_to_date_repo(up_to_date, tmpdir, store): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write(contents) - assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0 assert cfg.read() == contents -def test_autoupdate_old_revision_broken(tempdir_factory, in_tmpdir, store): +def test_autoupdate_old_revision_broken(tempdir_factory, in_tmpdir): """In $FUTURE_VERSION, hooks.yaml will no longer be supported. This asserts that when that day comes, pre-commit will be able to autoupdate despite not being able to read hooks.yaml in that repository. @@ -174,14 +174,14 @@ def test_autoupdate_old_revision_broken(tempdir_factory, in_tmpdir, store): write_config('.', config) with open(C.CONFIG_FILE) as f: before = f.read() - assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0 + assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 0 with open(C.CONFIG_FILE) as f: after = f.read() assert before != after assert update_rev in after -def test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store): +def test_autoupdate_out_of_date_repo(out_of_date, tmpdir): fmt = ( 'repos:\n' '- repo: {}\n' @@ -192,24 +192,24 @@ def test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write(fmt.format(out_of_date.path, out_of_date.original_rev)) - assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0 assert cfg.read() == fmt.format(out_of_date.path, out_of_date.head_rev) -def test_autoupdate_with_core_useBuiltinFSMonitor(out_of_date, tmpdir, store): +def test_autoupdate_with_core_useBuiltinFSMonitor(out_of_date, tmpdir): # force the setting on "globally" for git home = tmpdir.join('fakehome').ensure_dir() home.join('.gitconfig').write('[core]\nuseBuiltinFSMonitor = true\n') with envcontext.envcontext((('HOME', str(home)),)): - test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store) + test_autoupdate_out_of_date_repo(out_of_date, tmpdir) -def test_autoupdate_pure_yaml(out_of_date, tmpdir, store): +def test_autoupdate_pure_yaml(out_of_date, tmpdir): with mock.patch.object(yaml, 'Dumper', yaml.yaml.SafeDumper): - test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store) + test_autoupdate_out_of_date_repo(out_of_date, tmpdir) -def test_autoupdate_only_one_to_update(up_to_date, out_of_date, tmpdir, store): +def test_autoupdate_only_one_to_update(up_to_date, out_of_date, tmpdir): fmt = ( 'repos:\n' '- repo: {}\n' @@ -228,7 +228,7 @@ def test_autoupdate_only_one_to_update(up_to_date, out_of_date, tmpdir, store): ) cfg.write(before) - assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0 assert cfg.read() == fmt.format( up_to_date, git.head_rev(up_to_date), out_of_date.path, out_of_date.head_rev, @@ -236,7 +236,7 @@ def test_autoupdate_only_one_to_update(up_to_date, out_of_date, tmpdir, store): def test_autoupdate_out_of_date_repo_with_correct_repo_name( - out_of_date, in_tmpdir, store, + out_of_date, in_tmpdir, ): stale_config = make_config_from_repo( out_of_date.path, rev=out_of_date.original_rev, check=False, @@ -249,7 +249,7 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( before = f.read() repo_name = f'file://{out_of_date.path}' ret = autoupdate( - C.CONFIG_FILE, store, freeze=False, tags_only=False, + C.CONFIG_FILE, freeze=False, tags_only=False, repos=(repo_name,), ) with open(C.CONFIG_FILE) as f: @@ -261,7 +261,7 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( def test_autoupdate_out_of_date_repo_with_wrong_repo_name( - out_of_date, in_tmpdir, store, + out_of_date, in_tmpdir, ): config = make_config_from_repo( out_of_date.path, rev=out_of_date.original_rev, check=False, @@ -272,7 +272,7 @@ def test_autoupdate_out_of_date_repo_with_wrong_repo_name( before = f.read() # It will not update it, because the name doesn't match ret = autoupdate( - C.CONFIG_FILE, store, freeze=False, tags_only=False, + C.CONFIG_FILE, freeze=False, tags_only=False, repos=('dne',), ) with open(C.CONFIG_FILE) as f: @@ -281,7 +281,7 @@ def test_autoupdate_out_of_date_repo_with_wrong_repo_name( assert before == after -def test_does_not_reformat(tmpdir, out_of_date, store): +def test_does_not_reformat(tmpdir, out_of_date): fmt = ( 'repos:\n' '- repo: {}\n' @@ -294,12 +294,12 @@ def test_does_not_reformat(tmpdir, out_of_date, store): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write(fmt.format(out_of_date.path, out_of_date.original_rev)) - assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0 expected = fmt.format(out_of_date.path, out_of_date.head_rev) assert cfg.read() == expected -def test_does_not_change_mixed_endlines_read(up_to_date, tmpdir, store): +def test_does_not_change_mixed_endlines_read(up_to_date, tmpdir): fmt = ( 'repos:\n' '- repo: {}\n' @@ -314,11 +314,11 @@ def test_does_not_change_mixed_endlines_read(up_to_date, tmpdir, store): expected = fmt.format(up_to_date, git.head_rev(up_to_date)).encode() cfg.write_binary(expected) - assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0 assert cfg.read_binary() == expected -def test_does_not_change_mixed_endlines_write(tmpdir, out_of_date, store): +def test_does_not_change_mixed_endlines_write(tmpdir, out_of_date): fmt = ( 'repos:\n' '- repo: {}\n' @@ -333,12 +333,12 @@ def test_does_not_change_mixed_endlines_write(tmpdir, out_of_date, store): fmt.format(out_of_date.path, out_of_date.original_rev).encode(), ) - assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0 expected = fmt.format(out_of_date.path, out_of_date.head_rev).encode() assert cfg.read_binary() == expected -def test_loses_formatting_when_not_detectable(out_of_date, store, tmpdir): +def test_loses_formatting_when_not_detectable(out_of_date, tmpdir): """A best-effort attempt is made at updating rev without rewriting formatting. When the original formatting cannot be detected, this is abandoned. @@ -359,7 +359,7 @@ def test_loses_formatting_when_not_detectable(out_of_date, store, tmpdir): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write(config) - assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0 expected = ( f'repos:\n' f'- repo: {out_of_date.path}\n' @@ -370,43 +370,43 @@ def test_loses_formatting_when_not_detectable(out_of_date, store, tmpdir): assert cfg.read() == expected -def test_autoupdate_tagged_repo(tagged, in_tmpdir, store): +def test_autoupdate_tagged_repo(tagged, in_tmpdir): config = make_config_from_repo(tagged.path, rev=tagged.original_rev) write_config('.', config) - assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0 + assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 0 with open(C.CONFIG_FILE) as f: assert 'v1.2.3' in f.read() -def test_autoupdate_freeze(tagged, in_tmpdir, store): +def test_autoupdate_freeze(tagged, in_tmpdir): config = make_config_from_repo(tagged.path, rev=tagged.original_rev) write_config('.', config) - assert autoupdate(C.CONFIG_FILE, store, freeze=True, tags_only=False) == 0 + assert autoupdate(C.CONFIG_FILE, freeze=True, tags_only=False) == 0 with open(C.CONFIG_FILE) as f: expected = f'rev: {tagged.head_rev} # frozen: v1.2.3' assert expected in f.read() # if we un-freeze it should remove the frozen comment - assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0 + assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 0 with open(C.CONFIG_FILE) as f: assert 'rev: v1.2.3\n' in f.read() -def test_autoupdate_tags_only(tagged, in_tmpdir, store): +def test_autoupdate_tags_only(tagged, in_tmpdir): # add some commits after the tag git_commit(cwd=tagged.path) config = make_config_from_repo(tagged.path, rev=tagged.original_rev) write_config('.', config) - assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=True) == 0 + assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=True) == 0 with open(C.CONFIG_FILE) as f: assert 'v1.2.3' in f.read() -def test_autoupdate_latest_no_config(out_of_date, in_tmpdir, store): +def test_autoupdate_latest_no_config(out_of_date, in_tmpdir): config = make_config_from_repo( out_of_date.path, rev=out_of_date.original_rev, ) @@ -415,12 +415,12 @@ def test_autoupdate_latest_no_config(out_of_date, in_tmpdir, store): cmd_output('git', 'rm', '-r', ':/', cwd=out_of_date.path) git_commit(cwd=out_of_date.path) - assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 1 + assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 1 with open(C.CONFIG_FILE) as f: assert out_of_date.original_rev in f.read() -def test_hook_disppearing_repo_raises(hook_disappearing, store): +def test_hook_disppearing_repo_raises(hook_disappearing): config = make_config_from_repo( hook_disappearing.path, rev=hook_disappearing.original_rev, @@ -428,10 +428,10 @@ def test_hook_disppearing_repo_raises(hook_disappearing, store): ) info = RevInfo.from_config(config).update(tags_only=False, freeze=False) with pytest.raises(RepositoryCannotBeUpdatedError): - _check_hooks_still_exist_at_rev(config, info, store) + _check_hooks_still_exist_at_rev(config, info) -def test_autoupdate_hook_disappearing_repo(hook_disappearing, tmpdir, store): +def test_autoupdate_hook_disappearing_repo(hook_disappearing, tmpdir): contents = ( f'repos:\n' f'- repo: {hook_disappearing.path}\n' @@ -442,21 +442,21 @@ def test_autoupdate_hook_disappearing_repo(hook_disappearing, tmpdir, store): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write(contents) - assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 1 + assert autoupdate(str(cfg), freeze=False, tags_only=False) == 1 assert cfg.read() == contents -def test_autoupdate_local_hooks(in_git_dir, store): +def test_autoupdate_local_hooks(in_git_dir): config = sample_local_config() add_config_to_repo('.', config) - assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0 + assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 0 new_config_written = read_config('.') assert len(new_config_written['repos']) == 1 assert new_config_written['repos'][0] == config def test_autoupdate_local_hooks_with_out_of_date_repo( - out_of_date, in_tmpdir, store, + out_of_date, in_tmpdir, ): stale_config = make_config_from_repo( out_of_date.path, rev=out_of_date.original_rev, check=False, @@ -464,13 +464,13 @@ def test_autoupdate_local_hooks_with_out_of_date_repo( local_config = sample_local_config() config = {'repos': [local_config, stale_config]} write_config('.', config) - assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0 + assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 0 new_config_written = read_config('.') assert len(new_config_written['repos']) == 2 assert new_config_written['repos'][0] == local_config -def test_autoupdate_meta_hooks(tmpdir, store): +def test_autoupdate_meta_hooks(tmpdir): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write( 'repos:\n' @@ -478,7 +478,7 @@ def test_autoupdate_meta_hooks(tmpdir, store): ' hooks:\n' ' - id: check-useless-excludes\n', ) - assert autoupdate(str(cfg), store, freeze=False, tags_only=True) == 0 + assert autoupdate(str(cfg), freeze=False, tags_only=True) == 0 assert cfg.read() == ( 'repos:\n' '- repo: meta\n' @@ -487,7 +487,7 @@ def test_autoupdate_meta_hooks(tmpdir, store): ) -def test_updates_old_format_to_new_format(tmpdir, capsys, store): +def test_updates_old_format_to_new_format(tmpdir, capsys): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write( '- repo: local\n' @@ -497,7 +497,7 @@ def test_updates_old_format_to_new_format(tmpdir, capsys, store): ' entry: ./bin/foo.sh\n' ' language: script\n', ) - assert autoupdate(str(cfg), store, freeze=False, tags_only=True) == 0 + assert autoupdate(str(cfg), freeze=False, tags_only=True) == 0 contents = cfg.read() assert contents == ( 'repos:\n' @@ -512,7 +512,7 @@ def test_updates_old_format_to_new_format(tmpdir, capsys, store): assert out == 'Configuration has been migrated.\n' -def test_maintains_rev_quoting_style(tmpdir, out_of_date, store): +def test_maintains_rev_quoting_style(tmpdir, out_of_date): fmt = ( 'repos:\n' '- repo: {path}\n' @@ -527,6 +527,6 @@ def test_maintains_rev_quoting_style(tmpdir, out_of_date, store): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write(fmt.format(path=out_of_date.path, rev=out_of_date.original_rev)) - assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0 expected = fmt.format(path=out_of_date.path, rev=out_of_date.head_rev) assert cfg.read() == expected diff --git a/tests/commands/gc_test.py b/tests/commands/gc_test.py index c128e9393..95113ed5c 100644 --- a/tests/commands/gc_test.py +++ b/tests/commands/gc_test.py @@ -43,8 +43,9 @@ def test_gc(tempdir_factory, store, in_git_dir, cap_out): store.mark_config_used(C.CONFIG_FILE) # update will clone both the old and new repo, making the old one gc-able - install_hooks(C.CONFIG_FILE, store) - assert not autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) + assert not install_hooks(C.CONFIG_FILE, store) + assert not autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) + assert not install_hooks(C.CONFIG_FILE, store) assert _config_count(store) == 1 assert _repo_count(store) == 2 From ddbee32ad0722a0bc216bc29ee29a1885454bd78 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Apr 2023 15:05:17 -0400 Subject: [PATCH 799/967] add --jobs option to autoupdate --- pre_commit/commands/autoupdate.py | 92 +++++++++++++++++++------------ pre_commit/lang_base.py | 10 +--- pre_commit/main.py | 7 ++- pre_commit/xargs.py | 8 +++ 4 files changed, 73 insertions(+), 44 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 71e5c99b8..178648108 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -1,5 +1,6 @@ from __future__ import annotations +import concurrent.futures import os.path import re import tempfile @@ -10,6 +11,7 @@ import pre_commit.constants as C from pre_commit import git from pre_commit import output +from pre_commit import xargs from pre_commit.clientlib import InvalidManifestError from pre_commit.clientlib import load_config from pre_commit.clientlib import load_manifest @@ -71,7 +73,7 @@ def update(self, tags_only: bool, freeze: bool) -> RevInfo: try: manifest = load_manifest(os.path.join(tmp, C.MANIFEST_FILE)) except InvalidManifestError as e: - raise RepositoryCannotBeUpdatedError(str(e)) + raise RepositoryCannotBeUpdatedError(f'[{self.repo}] {e}') else: hook_ids = frozenset(hook['id'] for hook in manifest) @@ -91,11 +93,24 @@ def _check_hooks_still_exist_at_rev( hooks_missing = hooks - info.hook_ids if hooks_missing: raise RepositoryCannotBeUpdatedError( - f'Cannot update because the update target is missing these ' - f'hooks:\n{", ".join(sorted(hooks_missing))}', + f'[{info.repo}] Cannot update because the update target is ' + f'missing these hooks: {", ".join(sorted(hooks_missing))}', ) +def _update_one( + i: int, + repo: dict[str, Any], + *, + tags_only: bool, + freeze: bool, +) -> tuple[int, RevInfo, RevInfo]: + old = RevInfo.from_config(repo) + new = old.update(tags_only=tags_only, freeze=freeze) + _check_hooks_still_exist_at_rev(repo, new) + return i, old, new + + REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([\'"]?)([^\s#]+)(.*)(\r?\n)$') @@ -147,45 +162,50 @@ def autoupdate( tags_only: bool, freeze: bool, repos: Sequence[str] = (), + jobs: int = 1, ) -> int: """Auto-update the pre-commit config to the latest versions of repos.""" migrate_config(config_file, quiet=True) - retv = 0 - rev_infos: list[RevInfo | None] = [] changed = False + retv = 0 - config = load_config(config_file) - for repo_config in config['repos']: - if repo_config['repo'] in {LOCAL, META}: - continue - - info = RevInfo.from_config(repo_config) - if repos and info.repo not in repos: - rev_infos.append(None) - continue - - output.write(f'Updating {info.repo} ... ') - try: - new_info = info.update(tags_only=tags_only, freeze=freeze) - _check_hooks_still_exist_at_rev(repo_config, new_info) - except RepositoryCannotBeUpdatedError as error: - output.write_line(error.args[0]) - rev_infos.append(None) - retv = 1 - continue - - if new_info.rev != info.rev: - changed = True - if new_info.frozen: - updated_to = f'{new_info.frozen} (frozen)' + config_repos = [ + repo for repo in load_config(config_file)['repos'] + if repo['repo'] not in {LOCAL, META} + ] + + rev_infos: list[RevInfo | None] = [None] * len(config_repos) + jobs = jobs or xargs.cpu_count() # 0 => number of cpus + jobs = min(jobs, len(repos) or len(config_repos)) # max 1-per-thread + jobs = max(jobs, 1) # at least one thread + with concurrent.futures.ThreadPoolExecutor(jobs) as exe: + futures = [ + exe.submit( + _update_one, + i, repo, tags_only=tags_only, freeze=freeze, + ) + for i, repo in enumerate(config_repos) + if not repos or repo['repo'] in repos + ] + for future in concurrent.futures.as_completed(futures): + try: + i, old, new = future.result() + except RepositoryCannotBeUpdatedError as e: + output.write_line(str(e)) + retv = 1 else: - updated_to = new_info.rev - msg = f'updating {info.rev} -> {updated_to}.' - output.write_line(msg) - rev_infos.append(new_info) - else: - output.write_line('already up to date.') - rev_infos.append(None) + if new.rev != old.rev: + changed = True + if new.frozen: + new_s = f'{new.frozen} (frozen)' + else: + new_s = new.rev + msg = f'updating {old.rev} -> {new_s}' + rev_infos[i] = new + else: + msg = 'already up to date!' + + output.write_line(f'[{old.repo}] {msg}') if changed: _write_new_config(config_file, rev_infos) diff --git a/pre_commit/lang_base.py b/pre_commit/lang_base.py index 9480c559f..4a993eaa4 100644 --- a/pre_commit/lang_base.py +++ b/pre_commit/lang_base.py @@ -1,7 +1,6 @@ from __future__ import annotations import contextlib -import multiprocessing import os import random import re @@ -15,9 +14,9 @@ import pre_commit.constants as C from pre_commit import parse_shebang +from pre_commit import xargs from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b -from pre_commit.xargs import xargs FIXED_RANDOM_SEED = 1542676187 @@ -140,10 +139,7 @@ def target_concurrency() -> int: if 'TRAVIS' in os.environ: return 2 else: - try: - return multiprocessing.cpu_count() - except NotImplementedError: - return 1 + return xargs.cpu_count() def _shuffled(seq: Sequence[str]) -> list[str]: @@ -171,7 +167,7 @@ def run_xargs( # ordering. file_args = _shuffled(file_args) jobs = target_concurrency() - return xargs(cmd, file_args, target_concurrency=jobs, color=color) + return xargs.xargs(cmd, file_args, target_concurrency=jobs, color=color) def hook_cmd(entry: str, args: Sequence[str]) -> tuple[str, ...]: diff --git a/pre_commit/main.py b/pre_commit/main.py index 402bc2e56..9dfce2c25 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -226,9 +226,13 @@ def _add_cmd(name: str, *, help: str) -> argparse.ArgumentParser: help='Store "frozen" hashes in `rev` instead of tag names', ) autoupdate_parser.add_argument( - '--repo', dest='repos', action='append', metavar='REPO', + '--repo', dest='repos', action='append', metavar='REPO', default=[], help='Only update this repository -- may be specified multiple times.', ) + autoupdate_parser.add_argument( + '-j', '--jobs', type=int, default=1, + help='Number of threads to use. (default %(default)s).', + ) _add_cmd('clean', help='Clean out pre-commit files.') @@ -372,6 +376,7 @@ def _add_cmd(name: str, *, help: str) -> argparse.ArgumentParser: tags_only=not args.bleeding_edge, freeze=args.freeze, repos=args.repos, + jobs=args.jobs, ) elif args.command == 'clean': return clean(store) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index e3af90efd..31be6f323 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -3,6 +3,7 @@ import concurrent.futures import contextlib import math +import multiprocessing import os import subprocess import sys @@ -22,6 +23,13 @@ TRet = TypeVar('TRet') +def cpu_count() -> int: + try: + return multiprocessing.cpu_count() + except NotImplementedError: + return 1 + + def _environ_size(_env: MutableMapping[str, str] | None = None) -> int: environ = _env if _env is not None else getattr(os, 'environb', os.environ) size = 8 * len(environ) # number of pointers in `envp` From 4c0623963f9cd0735829fec265575fdd003a7659 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 1 May 2023 18:22:26 -0400 Subject: [PATCH 800/967] v3.3.0 --- CHANGELOG.md | 12 ++++++++++++ setup.cfg | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index efd96c796..57e58ff25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +3.3.0 - 2023-05-01 +================== + +### Features +- Upgrade ruby-build. + - #2846 PR by @jalessio. +- Use blobless clone for faster autoupdate. + - #2859 PR by @asottile. +- Add `-j` / `--jobs` argument to `autoupdate` for parallel execution. + - #2863 PR by @asottile. + - issue by @gaborbernat. + 3.2.2 - 2023-04-03 ================== diff --git a/setup.cfg b/setup.cfg index 89e8e4ada..8dffb6b75 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.2.2 +version = 3.3.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 420a15f87e6f0ec8f9fba0ff284b7e1bd34b9d82 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 2 May 2023 09:54:25 -0400 Subject: [PATCH 801/967] add partial clone hack to fix autoupdate for windows --- pre_commit/commands/autoupdate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 178648108..e7725fdc4 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -67,6 +67,8 @@ def update(self, tags_only: bool, freeze: bool) -> RevInfo: rev, frozen = exact, rev try: + # workaround for windows -- see #2865 + cmd_output_b(*_git, 'show', f'{rev}:{C.MANIFEST_FILE}') cmd_output(*_git, 'checkout', rev, '--', C.MANIFEST_FILE) except CalledProcessError: pass # this will be caught by manifest validating code From 51104fa94a6c3cdf603de2e187284289ea5abcf5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 2 May 2023 10:07:25 -0400 Subject: [PATCH 802/967] v3.3.1 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57e58ff25..970b8be19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +3.3.1 - 2023-05-02 +================== + +### Fixes +- Work around `git` partial clone bug for `autoupdate` on windows. + - #2866 PR by @asottile. + - #2865 issue by @adehad. + 3.3.0 - 2023-05-01 ================== diff --git a/setup.cfg b/setup.cfg index 8dffb6b75..cdd6ec3b2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.3.0 +version = 3.3.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 1dd85c904eb76543c80e6506cdfd662fcb889e3b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 9 May 2023 04:04:18 +0000 Subject: [PATCH 803/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - https://github.com/asottile/reorder_python_imports → https://github.com/asottile/reorder-python-imports - [github.com/asottile/pyupgrade: v3.3.2 → v3.4.0](https://github.com/asottile/pyupgrade/compare/v3.3.2...v3.4.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ffd305878..d275d244e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ repos: rev: v2.2.0 hooks: - id: setup-cfg-fmt -- repo: https://github.com/asottile/reorder_python_imports +- repo: https://github.com/asottile/reorder-python-imports rev: v3.9.0 hooks: - id: reorder-python-imports @@ -25,7 +25,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v3.3.2 + rev: v3.4.0 hooks: - id: pyupgrade args: [--py38-plus] From 8923fa368a5cb37ed7219a7ce0eafbe2351258b5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 13 May 2023 15:46:34 -0400 Subject: [PATCH 804/967] r does not support language_version currently --- pre_commit/languages/r.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 138a26e1e..083329c0e 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -93,6 +93,8 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: + lang_base.assert_version_default('r', version) + env_dir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) os.makedirs(env_dir, exist_ok=True) shutil.copy(prefix.path('renv.lock'), env_dir) From 926071b6a7e9f797cab6a089e32cd59065741b1e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 13 May 2023 16:03:14 -0400 Subject: [PATCH 805/967] make some files trigger all languages --- testing/languages | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/testing/languages b/testing/languages index 5e8fc9e4f..9abc185f1 100755 --- a/testing/languages +++ b/testing/languages @@ -16,6 +16,15 @@ EXCLUDED = frozenset(( )) +def _always_run() -> frozenset[str]: + ret = ['.github/workflows/languages.yml', 'testing/languages'] + ret.extend( + os.path.join('pre_commit/resources', fname) + for fname in os.listdir('pre_commit/resources') + ) + return frozenset(ret) + + def _lang_files(lang: str) -> frozenset[str]: prog = f'''\ import json @@ -47,10 +56,12 @@ def main() -> int: if fname.endswith('.py') and fname != '__init__.py' ] + triggers_all = _always_run() + if not args.all: with concurrent.futures.ThreadPoolExecutor(os.cpu_count()) as exe: by_lang = { - lang: files + lang: files | triggers_all for lang, files in zip(langs, exe.map(_lang_files, langs)) } From 9c2a01186b0b7e3395dfa2744e94a1b860ef716f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 13 May 2023 16:27:14 -0400 Subject: [PATCH 806/967] fix typo in testing/languages --- testing/languages | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/testing/languages b/testing/languages index 9abc185f1..f4804c7e5 100755 --- a/testing/languages +++ b/testing/languages @@ -17,7 +17,7 @@ EXCLUDED = frozenset(( def _always_run() -> frozenset[str]: - ret = ['.github/workflows/languages.yml', 'testing/languages'] + ret = ['.github/workflows/languages.yaml', 'testing/languages'] ret.extend( os.path.join('pre_commit/resources', fname) for fname in os.listdir('pre_commit/resources') @@ -57,6 +57,8 @@ def main() -> int: ] triggers_all = _always_run() + for fname in triggers_all: + assert os.path.exists(fname), fname if not args.all: with concurrent.futures.ThreadPoolExecutor(os.cpu_count()) as exe: From 08b670ff9e32e6c0fca2c5d22180b8b686b3d985 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 13 May 2023 16:24:29 -0400 Subject: [PATCH 807/967] swift is included in github actions --- .github/workflows/languages.yaml | 2 -- testing/get-swift.sh | 29 ----------------------------- 2 files changed, 31 deletions(-) delete mode 100755 testing/get-swift.sh diff --git a/.github/workflows/languages.yaml b/.github/workflows/languages.yaml index 8bc8e712f..57a1c0c79 100644 --- a/.github/workflows/languages.yaml +++ b/.github/workflows/languages.yaml @@ -63,8 +63,6 @@ jobs: echo 'C:\Strawberry\c\bin' >> "$GITHUB_PATH" shell: bash if: matrix.os == 'windows-latest' && matrix.language == 'perl' - - run: testing/get-swift.sh - if: matrix.os == 'ubuntu-latest' && matrix.language == 'swift' - name: install deps run: python -mpip install -e . -r requirements-dev.txt diff --git a/testing/get-swift.sh b/testing/get-swift.sh deleted file mode 100755 index dfe093912..000000000 --- a/testing/get-swift.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -# This is a script used in CI to install swift -set -euo pipefail - -. /etc/lsb-release -if [ "$DISTRIB_CODENAME" = "jammy" ]; then - SWIFT_URL='https://download.swift.org/swift-5.7.1-release/ubuntu2204/swift-5.7.1-RELEASE/swift-5.7.1-RELEASE-ubuntu22.04.tar.gz' - SWIFT_HASH='7f60291f5088d3e77b0c2364beaabd29616ee7b37260b7b06bdbeb891a7fe161' -else - echo "unknown dist: ${DISTRIB_CODENAME}" 1>&2 - exit 1 -fi - -check() { - echo "$SWIFT_HASH $TGZ" | sha256sum --check -} - -TGZ="$HOME/.swift/swift.tar.gz" -mkdir -p "$(dirname "$TGZ")" -if ! check >& /dev/null; then - rm -f "$TGZ" - curl --location --silent --output "$TGZ" "$SWIFT_URL" - check -fi - -mkdir -p /tmp/swift -tar -xf "$TGZ" --strip 1 --directory /tmp/swift - -echo '/tmp/swift/usr/bin' >> "$GITHUB_PATH" From 64985bd63d62126dc4efd58af63967ae80121ca2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 16 May 2023 03:20:34 +0000 Subject: [PATCH 808/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.2.0 → v1.3.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.2.0...v1.3.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d275d244e..cb03c759d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.2.0 + rev: v1.3.0 hooks: - id: mypy additional_dependencies: [types-all] From cd09c3525e35676af9fa614c04faebe5a88fc9de Mon Sep 17 00:00:00 2001 From: Lorenz Walthert Date: Mon, 15 May 2023 09:26:55 +0200 Subject: [PATCH 809/967] avoid quoting and escaping while installing R hooks by writing code to tempfile instead of execute R code inline --- pre_commit/languages/r.py | 51 +++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 083329c0e..6feb06523 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -4,6 +4,8 @@ import os import shlex import shutil +import tempfile +import textwrap from typing import Generator from typing import Sequence @@ -21,6 +23,19 @@ health_check = lang_base.basic_health_check +@contextlib.contextmanager +def _r_code_in_tempfile(code: str) -> Generator[str, None, None]: + """ + To avoid quoting and escaping issues, avoid `Rscript [options] -e {expr}` + but use `Rscript [options] path/to/file_with_expr.R` + """ + with tempfile.TemporaryDirectory() as tmpdir: + fname = os.path.join(tmpdir, 'script.R') + with open(fname, 'w') as f: + f.write(_inline_r_setup(textwrap.dedent(code))) + yield fname + + def get_env_patch(venv: str) -> PatchesT: return ( ('R_PROFILE_USER', os.path.join(venv, 'activate.R')), @@ -129,20 +144,19 @@ def install_environment( }} """ - cmd_output_b( - _rscript_exec(), '--vanilla', '-e', - _inline_r_setup(r_code_inst_environment), - cwd=env_dir, - ) + with _r_code_in_tempfile(r_code_inst_environment) as f: + cmd_output_b(_rscript_exec(), '--vanilla', f, cwd=env_dir) + if additional_dependencies: r_code_inst_add = 'renv::install(commandArgs(trailingOnly = TRUE))' with in_env(prefix, version): - cmd_output_b( - _rscript_exec(), *RSCRIPT_OPTS, '-e', - _inline_r_setup(r_code_inst_add), - *additional_dependencies, - cwd=env_dir, - ) + with _r_code_in_tempfile(r_code_inst_add) as f: + cmd_output_b( + _rscript_exec(), *RSCRIPT_OPTS, + f, + *additional_dependencies, + cwd=env_dir, + ) def _inline_r_setup(code: str) -> str: @@ -150,11 +164,16 @@ def _inline_r_setup(code: str) -> str: Some behaviour of R cannot be configured via env variables, but can only be configured via R options once R has started. These are set here. """ - with_option = f"""\ - options(install.packages.compile.from.source = "never", pkgType = "binary") - {code} - """ - return with_option + with_option = [ + textwrap.dedent("""\ + options( + install.packages.compile.from.source = "never", + pkgType = "binary" + ) + """), + code, + ] + return '\n'.join(with_option) def run_hook( From a0a734750e1af5a0ec0b2579d3b05f427f53c8b6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 17 May 2023 18:36:52 -0400 Subject: [PATCH 810/967] v3.3.2 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 970b8be19..4256c6aa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +3.3.2 - 2023-05-17 +================== + +### Fixes +- Work around `r` on windows sometimes double-un-quoting arguments. + - #2885 PR by @lorenzwalthert. + - #2870 issue by @lorenzwalthert. + 3.3.1 - 2023-05-02 ================== diff --git a/setup.cfg b/setup.cfg index cdd6ec3b2..b2c268ba8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.3.1 +version = 3.3.2 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 18348f5d0dbbfe10b139dbd8f220fe608810fc83 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 17 May 2023 18:55:11 -0400 Subject: [PATCH 811/967] use distlib inside the zipapp docker image --- testing/zipapp/Dockerfile | 4 ++-- testing/zipapp/make | 16 +++++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/testing/zipapp/Dockerfile b/testing/zipapp/Dockerfile index 7c74c1b2e..ea967e383 100644 --- a/testing/zipapp/Dockerfile +++ b/testing/zipapp/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:focal +FROM ubuntu:jammy RUN : \ && apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ @@ -11,4 +11,4 @@ RUN : \ ENV LANG=C.UTF-8 PATH=/venv/bin:$PATH RUN : \ && python3 -mvenv /venv \ - && pip install --no-cache-dir pip setuptools wheel no-manylinux --upgrade + && pip install --no-cache-dir pip distlib no-manylinux --upgrade diff --git a/testing/zipapp/make b/testing/zipapp/make index 37b5c355d..165046f66 100755 --- a/testing/zipapp/make +++ b/testing/zipapp/make @@ -4,7 +4,6 @@ from __future__ import annotations import argparse import base64 import hashlib -import importlib.resources import io import os.path import shutil @@ -42,10 +41,17 @@ def _add_shim(dest: str) -> None: with zipfile.ZipFile(bio, 'w') as zipf: zipf.write(shim, arcname='__main__.py') - with open(os.path.join(dest, 'python.exe'), 'wb') as f: - f.write(importlib.resources.read_binary('distlib', 't32.exe')) - f.write(b'#!py.exe -3\n') - f.write(bio.getvalue()) + with tempfile.TemporaryDirectory() as tmpdir: + _exit_if_retv( + 'podman', 'run', '--rm', '--volume', f'{tmpdir}:/out:rw', IMG, + 'cp', '/venv/lib/python3.10/site-packages/distlib/t32.exe', '/out', + ) + + with open(os.path.join(dest, 'python.exe'), 'wb') as f: + with open(os.path.join(tmpdir, 't32.exe'), 'rb') as t32: + f.write(t32.read()) + f.write(b'#!py.exe -3\n') + f.write(bio.getvalue()) def _write_cache_key(version: str, wheeldir: str, dest: str) -> None: From f4a2d52bb46f9d030aad76790035b5a3c12cb1cb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 1 Jun 2023 19:12:46 -0400 Subject: [PATCH 812/967] fix tags trigger for github actions the old syntax worked for azure pipelines but not GHA Committed via https://github.com/asottile/all-repos --- .github/workflows/languages.yaml | 2 +- .github/workflows/main.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/languages.yaml b/.github/workflows/languages.yaml index 57a1c0c79..7e97158cf 100644 --- a/.github/workflows/languages.yaml +++ b/.github/workflows/languages.yaml @@ -3,7 +3,7 @@ name: languages on: push: branches: [main, test-me-*] - tags: + tags: '*' pull_request: concurrency: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f281dcf27..903d24780 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,7 +3,7 @@ name: main on: push: branches: [main, test-me-*] - tags: + tags: '*' pull_request: concurrency: From f88cc6125681378d4d2704a7b08dd595e3744180 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 6 Jun 2023 03:14:12 +0000 Subject: [PATCH 813/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v2.2.0 → v2.3.0](https://github.com/asottile/setup-cfg-fmt/compare/v2.2.0...v2.3.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cb03c759d..80ee23d00 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.2.0 + rev: v2.3.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports From 5d273951e00e8331b6b3b07ef6e0280080566cca Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 6 Jun 2023 03:14:26 +0000 Subject: [PATCH 814/967] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index b2c268ba8..efbf2141d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,7 +8,7 @@ url = https://github.com/pre-commit/pre-commit author = Anthony Sottile author_email = asottile@umich.edu license = MIT -license_file = LICENSE +license_files = LICENSE classifiers = License :: OSI Approved :: MIT License Programming Language :: Python :: 3 From 1fc28903ab82e4ef7f8e9c37b052e4f9a53c9967 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 13 Jun 2023 03:58:21 +0000 Subject: [PATCH 815/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/add-trailing-comma: v2.4.0 → v2.5.1](https://github.com/asottile/add-trailing-comma/compare/v2.4.0...v2.5.1) - [github.com/asottile/pyupgrade: v3.4.0 → v3.6.0](https://github.com/asottile/pyupgrade/compare/v3.4.0...v3.6.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 80ee23d00..7810d2294 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,12 +20,12 @@ repos: exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) args: [--py38-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma - rev: v2.4.0 + rev: v2.5.1 hooks: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v3.4.0 + rev: v3.6.0 hooks: - id: pyupgrade args: [--py38-plus] From 9a7ed8be09b6de99bae5d1e03defc350a132b1e5 Mon Sep 17 00:00:00 2001 From: Jay Soffian Date: Tue, 13 Jun 2023 17:47:49 -0400 Subject: [PATCH 816/967] Force gem installation into envdir RubyGems allows OS packagers to specify defaults for `--install-dir` and `--bindir` and these take precedence over `GEM_HOME`. The only way to override the defaults is to explicitly specify the options ourselves when running `gem install`. Examples of OSes where this is the case are RedHat 9.2 and Gentoo. Fixes #2799. --- pre_commit/languages/ruby.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 76631f253..a411925a2 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -114,6 +114,8 @@ def _install_ruby( def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) + if version != 'system': # pragma: win32 no cover _install_rbenv(prefix, version) with in_env(prefix, version): @@ -135,6 +137,8 @@ def install_environment( 'gem', 'install', '--no-document', '--no-format-executable', '--no-user-install', + '--install-dir', os.path.join(envdir, 'gems'), + '--bindir', os.path.join(envdir, 'gems', 'bin'), *prefix.star('.gem'), *additional_dependencies, ), ) From 50b1511a5b81e5c95bcf496acc22dc9799a429b3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 13 Jun 2023 22:04:03 +0000 Subject: [PATCH 817/967] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pre_commit/languages/ruby.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index a411925a2..c88269f24 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -138,7 +138,7 @@ def install_environment( '--no-document', '--no-format-executable', '--no-user-install', '--install-dir', os.path.join(envdir, 'gems'), - '--bindir', os.path.join(envdir, 'gems', 'bin'), + '--bindir', os.path.join(envdir, 'gems', 'bin'), *prefix.star('.gem'), *additional_dependencies, ), ) From 5da4258b17dea7bd4601358de200e185699f9997 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 13 Jun 2023 19:11:02 -0400 Subject: [PATCH 818/967] v3.3.3 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4256c6aa4..722e8ffa3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +3.3.3 - 2023-06-13 +================== + +### Fixes +- Work around OS packagers setting `--install-dir` / `--bin-dir` in gem settings. + - #2905 PR by @jaysoffian. + - #2799 issue by @lmilbaum. + 3.3.2 - 2023-05-17 ================== diff --git a/setup.cfg b/setup.cfg index efbf2141d..88302e752 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.3.2 +version = 3.3.3 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From f94744a699e7d125bcd7cabc070c3129a9079cc1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 Jun 2023 04:33:31 +0000 Subject: [PATCH 819/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder-python-imports: v3.9.0 → v3.10.0](https://github.com/asottile/reorder-python-imports/compare/v3.9.0...v3.10.0) - [github.com/asottile/pyupgrade: v3.6.0 → v3.7.0](https://github.com/asottile/pyupgrade/compare/v3.6.0...v3.7.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7810d2294..6896fb75a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports - rev: v3.9.0 + rev: v3.10.0 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) @@ -25,7 +25,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v3.6.0 + rev: v3.7.0 hooks: - id: pyupgrade args: [--py38-plus] From 854f6985314079889586d1eee43fc185fe0fee62 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 27 Jun 2023 03:51:58 +0000 Subject: [PATCH 820/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.3.0 → v1.4.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.3.0...v1.4.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6896fb75a..bb989bb34 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.3.0 + rev: v1.4.1 hooks: - id: mypy additional_dependencies: [types-all] From e72699b9ef824d1dc2a1834ba7acbced9853235a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 1 Jul 2023 16:47:06 -0400 Subject: [PATCH 821/967] updates for add-trailing-comma 3.x Committed via https://github.com/asottile/all-repos --- .pre-commit-config.yaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bb989bb34..2bd6cca98 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.3.0 + rev: v2.4.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports @@ -20,12 +20,11 @@ repos: exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) args: [--py38-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma - rev: v2.5.1 + rev: v3.0.0 hooks: - id: add-trailing-comma - args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v3.7.0 + rev: v3.8.0 hooks: - id: pyupgrade args: [--py38-plus] From 1c439b5a79d1c6ff9e36327f852e58e79452124c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 1 Jul 2023 17:22:42 -0400 Subject: [PATCH 822/967] shlex.join is always available in 3.8+ --- pre_commit/commands/install_uninstall.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 5ff6cba6e..d19e0d47e 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -103,8 +103,7 @@ def _install_hook_script( hook_file.write(before + TEMPLATE_START) hook_file.write(f'INSTALL_PYTHON={shlex.quote(sys.executable)}\n') - # TODO: python3.8+: shlex.join - args_s = ' '.join(shlex.quote(part) for part in args) + args_s = shlex.join(args) hook_file.write(f'ARGS=({args_s})\n') hook_file.write(TEMPLATE_END + after) make_executable(hook_path) From 9bf6856db35f51be1fd131094aba142f71af3543 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 11 Jul 2023 05:21:22 +0000 Subject: [PATCH 823/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.8.0 → v3.9.0](https://github.com/asottile/pyupgrade/compare/v3.8.0...v3.9.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2bd6cca98..2e7ff8cc9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.8.0 + rev: v3.9.0 hooks: - id: pyupgrade args: [--py38-plus] From 5e4af63e8546f9b0e3f9b4a454b09c8607d02fe8 Mon Sep 17 00:00:00 2001 From: Max R Date: Sun, 16 Jul 2023 15:00:55 -0400 Subject: [PATCH 824/967] Fix link to `language` API --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ab3a92989..182e7bc10 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -92,7 +92,7 @@ language, for example: here are the apis that should be implemented for a language -Note that these are also documented in [`pre_commit/languages/all.py`](https://github.com/pre-commit/pre-commit/blob/main/pre_commit/languages/all.py) +Note that these are also documented in [`pre_commit/lang_base.py`](https://github.com/pre-commit/pre-commit/blob/main/pre_commit/lang_base.py) #### `ENVIRONMENT_DIR` From d537c09032e1c1ca945aec2d0abb8fe80835eb88 Mon Sep 17 00:00:00 2001 From: Max R Date: Mon, 17 Jul 2023 09:36:47 -0400 Subject: [PATCH 825/967] `s/helpers/lang_base/g` --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ab3a92989..dc7a70c7b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -111,7 +111,7 @@ one cannot be determined, return `'default'`. You generally don't need to implement this on a first pass and can just use: ```python -get_default_version = helpers.basic_default_version +get_default_version = lang_base.basic_default_version ``` `python` is currently the only language which implements this api @@ -125,7 +125,7 @@ healthy. You generally don't need to implement this on a first pass and can just use: ```python -health_check = helpers.basic_healthy_check +health_check = lang_base.basic_healthy_check ``` `python` is currently the only language which implements this api, for python @@ -137,7 +137,7 @@ this is the trickiest one to implement and where all the smart parts happen. this api should do the following things -- (0th / 3rd class): `install_environment = helpers.no_install` +- (0th / 3rd class): `install_environment = lang_base.no_install` - (1st class): install a language runtime into the hook's directory - (2nd class): install the package at `.` into the `ENVIRONMENT_DIR` - (2nd class, optional): install packages listed in `additional_dependencies` From 60273ca81ea974bd429e7ebfbcee6b8598f30040 Mon Sep 17 00:00:00 2001 From: Alex Brandt Date: Wed, 19 Jul 2023 19:26:28 +0100 Subject: [PATCH 826/967] Add haskell language support to pre-commit. --- .github/workflows/languages.yaml | 2 ++ pre_commit/all_languages.py | 2 ++ pre_commit/languages/haskell.py | 56 ++++++++++++++++++++++++++++++++ tests/languages/haskell_test.py | 50 ++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+) create mode 100644 pre_commit/languages/haskell.py create mode 100644 tests/languages/haskell_test.py diff --git a/.github/workflows/languages.yaml b/.github/workflows/languages.yaml index 7e97158cf..5a6ae9cd7 100644 --- a/.github/workflows/languages.yaml +++ b/.github/workflows/languages.yaml @@ -63,6 +63,8 @@ jobs: echo 'C:\Strawberry\c\bin' >> "$GITHUB_PATH" shell: bash if: matrix.os == 'windows-latest' && matrix.language == 'perl' + - uses: haskell/actions/setup@v2 + if: matrix.language == 'haskell' - name: install deps run: python -mpip install -e . -r requirements-dev.txt diff --git a/pre_commit/all_languages.py b/pre_commit/all_languages.py index 2bed7067f..476bad9da 100644 --- a/pre_commit/all_languages.py +++ b/pre_commit/all_languages.py @@ -9,6 +9,7 @@ from pre_commit.languages import dotnet from pre_commit.languages import fail from pre_commit.languages import golang +from pre_commit.languages import haskell from pre_commit.languages import lua from pre_commit.languages import node from pre_commit.languages import perl @@ -31,6 +32,7 @@ 'dotnet': dotnet, 'fail': fail, 'golang': golang, + 'haskell': haskell, 'lua': lua, 'node': node, 'perl': perl, diff --git a/pre_commit/languages/haskell.py b/pre_commit/languages/haskell.py new file mode 100644 index 000000000..76442eb02 --- /dev/null +++ b/pre_commit/languages/haskell.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +import contextlib +import os.path +from typing import Generator +from typing import Sequence + +from pre_commit import lang_base +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import Var +from pre_commit.errors import FatalError +from pre_commit.prefix import Prefix + +ENVIRONMENT_DIR = 'hs_env' +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook + + +def get_env_patch(target_dir: str) -> PatchesT: + bin_path = os.path.join(target_dir, 'bin') + return (('PATH', (bin_path, os.pathsep, Var('PATH'))),) + + +@contextlib.contextmanager +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) + with envcontext(get_env_patch(envdir)): + yield + + +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: + lang_base.assert_version_default('haskell', version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) + + pkgs = [*prefix.star('.cabal'), *additional_dependencies] + if not pkgs: + raise FatalError('Expected .cabal files or additional_dependencies') + + bindir = os.path.join(envdir, 'bin') + os.makedirs(bindir, exist_ok=True) + lang_base.setup_cmd(prefix, ('cabal', 'update')) + lang_base.setup_cmd( + prefix, + ( + 'cabal', 'install', + '--install-method', 'copy', + '--installdir', bindir, + *pkgs, + ), + ) diff --git a/tests/languages/haskell_test.py b/tests/languages/haskell_test.py new file mode 100644 index 000000000..f888109bd --- /dev/null +++ b/tests/languages/haskell_test.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +import pytest + +from pre_commit.errors import FatalError +from pre_commit.languages import haskell +from pre_commit.util import win_exe +from testing.language_helpers import run_language + + +def test_run_example_executable(tmp_path): + example_cabal = '''\ +cabal-version: 2.4 +name: example +version: 0.1.0.0 + +executable example + main-is: Main.hs + + build-depends: base >=4 + default-language: Haskell2010 +''' + main_hs = '''\ +module Main where + +main :: IO () +main = putStrLn "Hello, Haskell!" +''' + tmp_path.joinpath('example.cabal').write_text(example_cabal) + tmp_path.joinpath('Main.hs').write_text(main_hs) + + result = run_language(tmp_path, haskell, 'example') + assert result == (0, b'Hello, Haskell!\n') + + # should not symlink things into environments + exe = tmp_path.joinpath(win_exe('hs_env-default/bin/example')) + assert exe.is_file() + assert not exe.is_symlink() + + +def test_run_dep(tmp_path): + result = run_language(tmp_path, haskell, 'hello', deps=['hello']) + assert result == (0, b'Hello, World!\n') + + +def test_run_empty(tmp_path): + with pytest.raises(FatalError) as excinfo: + run_language(tmp_path, haskell, 'example') + msg, = excinfo.value.args + assert msg == 'Expected .cabal files or additional_dependencies' From 3557077bbc9a5c7fe8f373f785ec2d2d79b6a999 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 1 Aug 2023 07:08:53 +0000 Subject: [PATCH 827/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/add-trailing-comma: v3.0.0 → v3.0.1](https://github.com/asottile/add-trailing-comma/compare/v3.0.0...v3.0.1) - [github.com/asottile/pyupgrade: v3.9.0 → v3.10.1](https://github.com/asottile/pyupgrade/compare/v3.9.0...v3.10.1) - [github.com/PyCQA/flake8: 6.0.0 → 6.1.0](https://github.com/PyCQA/flake8/compare/6.0.0...6.1.0) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2e7ff8cc9..4ab4feb3e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,11 +20,11 @@ repos: exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) args: [--py38-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma - rev: v3.0.0 + rev: v3.0.1 hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.9.0 + rev: v3.10.1 hooks: - id: pyupgrade args: [--py38-plus] @@ -33,7 +33,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + rev: 6.1.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy From 8c75a26f2df489b89e808d26f0cdd83ced19d1e0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 1 Aug 2023 12:08:52 -0400 Subject: [PATCH 828/967] update hello world go test --- tests/languages/golang_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py index ec5a87875..640626711 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -128,7 +128,7 @@ def test_local_golang_additional_deps(tmp_path): deps=('golang.org/x/example/hello@latest',), ) - assert ret == (0, b'Hello, Go examples!\n') + assert ret == (0, b'Hello, world!\n') def test_golang_hook_still_works_when_gobin_is_set(tmp_path): From 1803db979f86ab3e1df8194f40f0177413f0fbb3 Mon Sep 17 00:00:00 2001 From: Fufu Fang Date: Mon, 14 Aug 2023 11:00:17 +0100 Subject: [PATCH 829/967] fix typo in CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9965c6ca0..da7f9432f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -125,7 +125,7 @@ healthy. You generally don't need to implement this on a first pass and can just use: ```python -health_check = lang_base.basic_healthy_check +health_check = lang_base.basic_health_check ``` `python` is currently the only language which implements this api, for python From 93b1a144023891c0083e2a18cd8d320e47e0d656 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 15 Aug 2023 06:03:09 +0000 Subject: [PATCH 830/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.4.1 → v1.5.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.4.1...v1.5.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4ab4feb3e..b53a90e29 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.4.1 + rev: v1.5.0 hooks: - id: mypy additional_dependencies: [types-all] From 5a4b5b1f8ea29a9df154df504f844db186e877b0 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Mon, 21 Aug 2023 20:02:27 -0500 Subject: [PATCH 831/967] Fix exit code for commands terminated by signals Fixes https://github.com/pre-commit/pre-commit/issues/2970 --- pre_commit/xargs.py | 3 ++- tests/xargs_test.py | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 31be6f323..eff57ce7c 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -170,7 +170,8 @@ def run_cmd_partition( results = thread_map(run_cmd_partition, partitions) for proc_retcode, proc_out, _ in results: - retcode = max(retcode, proc_retcode) + if abs(proc_retcode) > abs(retcode): + retcode = proc_retcode stdout += proc_out return retcode, stdout diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 7c41f98cd..b0a8e0d66 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -147,6 +147,15 @@ def test_xargs_retcode_normal(): assert ret == 5 +@pytest.mark.xfail(sys.platform == 'win32', reason='posix only') +def test_xargs_retcode_killed_by_signal(): + ret, _ = xargs.xargs( + parse_shebang.normalize_cmd(('bash', '-c', 'kill -9 $$', '--')), + ('foo', 'bar'), + ) + assert ret == -9 + + def test_xargs_concurrency(): bash_cmd = parse_shebang.normalize_cmd(('bash', '-c')) print_pid = ('sleep 0.5 && echo $$',) From a4ae868633ca56f37fb4264c528c2ae52f50305f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 06:16:21 +0000 Subject: [PATCH 832/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.5.0 → v1.5.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.5.0...v1.5.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b53a90e29..54a56ef38 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.5.0 + rev: v1.5.1 hooks: - id: mypy additional_dependencies: [types-all] From 3dd1875df85ea258c790af93ed9d4311fc87a5d8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 29 Aug 2023 05:38:11 +0000 Subject: [PATCH 833/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-autopep8: v2.0.2 → v2.0.4](https://github.com/pre-commit/mirrors-autopep8/compare/v2.0.2...v2.0.4) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 54a56ef38..5c6f62b45 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v2.0.2 + rev: v2.0.4 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 From ea8244b229fa3b7e0c1c26e5e824fb64dfbb4b1d Mon Sep 17 00:00:00 2001 From: Joe Bateson Date: Mon, 28 Aug 2023 19:20:23 -0700 Subject: [PATCH 834/967] Use os.sched_getaffinity for cpu counts when available --- pre_commit/xargs.py | 8 ++++++++ tests/lang_base_test.py | 27 +++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index eff57ce7c..a7493c01d 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -24,6 +24,14 @@ def cpu_count() -> int: + try: + # On systems that support it, this will return a more accurate count of + # usable CPUs for the current process, which will take into account + # cgroup limits + return len(os.sched_getaffinity(0)) + except AttributeError: + pass + try: return multiprocessing.cpu_count() except NotImplementedError: diff --git a/tests/lang_base_test.py b/tests/lang_base_test.py index a532b6a54..1cffa0e5f 100644 --- a/tests/lang_base_test.py +++ b/tests/lang_base_test.py @@ -30,6 +30,19 @@ def fake_expanduser(pth): yield +@pytest.fixture +def no_sched_getaffinity(): + # Simulates an OS without os.sched_getaffinity available (mac/windows) + # https://docs.python.org/3/library/os.html#interface-to-the-scheduler + with mock.patch.object( + os, + 'sched_getaffinity', + create=True, + side_effect=AttributeError, + ): + yield + + def test_exe_exists_does_not_exist(find_exe_mck, homedir_mck): find_exe_mck.return_value = None assert lang_base.exe_exists('ruby') is False @@ -116,7 +129,17 @@ def test_no_env_noop(tmp_path): assert before == inside == after -def test_target_concurrency_normal(): +def test_target_concurrency_sched_getaffinity(no_sched_getaffinity): + with mock.patch.object( + os, + 'sched_getaffinity', + return_value=set(range(345)), + ): + with mock.patch.dict(os.environ, clear=True): + assert lang_base.target_concurrency() == 345 + + +def test_target_concurrency_without_sched_getaffinity(no_sched_getaffinity): with mock.patch.object(multiprocessing, 'cpu_count', return_value=123): with mock.patch.dict(os.environ, {}, clear=True): assert lang_base.target_concurrency() == 123 @@ -134,7 +157,7 @@ def test_target_concurrency_on_travis(): assert lang_base.target_concurrency() == 2 -def test_target_concurrency_cpu_count_not_implemented(): +def test_target_concurrency_cpu_count_not_implemented(no_sched_getaffinity): with mock.patch.object( multiprocessing, 'cpu_count', side_effect=NotImplementedError, ): From fe9ba6b53fd5ae112ef5a3d2ac883e2d0e5a10db Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 2 Sep 2023 13:09:13 -0400 Subject: [PATCH 835/967] v3.4.0 --- CHANGELOG.md | 15 +++++++++++++++ setup.cfg | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 722e8ffa3..9e2ef0de1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +3.4.0 - 2023-09-02 +================== + +### Features +- Add `language: haskell`. + - #2932 by @alunduil. +- Improve cpu count detection when run under cgroups. + - #2979 PR by @jdb8. + - #2978 issue by @jdb8. + +### Fixes +- Handle negative exit codes from hooks receiving posix signals. + - #2971 PR by @chriskuehl. + - #2970 issue by @chriskuehl. + 3.3.3 - 2023-06-13 ================== diff --git a/setup.cfg b/setup.cfg index 88302e752..cfaa61bbb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.3.3 +version = 3.4.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 818240e42575620ba8d8d5f36c1d7b5765699c68 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 06:46:49 +0000 Subject: [PATCH 836/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/add-trailing-comma: v3.0.1 → v3.1.0](https://github.com/asottile/add-trailing-comma/compare/v3.0.1...v3.1.0) - https://github.com/pre-commit/mirrors-autopep8 → https://github.com/hhatto/autopep8 --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5c6f62b45..3b98f96bc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) args: [--py38-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma - rev: v3.0.1 + rev: v3.1.0 hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade @@ -28,7 +28,7 @@ repos: hooks: - id: pyupgrade args: [--py38-plus] -- repo: https://github.com/pre-commit/mirrors-autopep8 +- repo: https://github.com/hhatto/autopep8 rev: v2.0.4 hooks: - id: autopep8 From 493c20ce91818493068e499216e64709b96f1230 Mon Sep 17 00:00:00 2001 From: Roel Adriaans Date: Fri, 8 Sep 2023 15:12:45 +0200 Subject: [PATCH 837/967] Use the --include command, hides warning messages Fixes #1983 --- pre_commit/languages/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 66d613637..3e22dc78e 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -93,7 +93,7 @@ def install_environment( # install as if we installed from git local_install_cmd = ( - 'npm', 'install', '--dev', '--prod', + 'npm', 'install', '--include=dev', '--include=prod', '--ignore-prepublish', '--no-progress', '--no-save', ) lang_base.setup_cmd(prefix, local_install_cmd) From 9ac229dad886ed5b133a946a524f36ce4220cbf9 Mon Sep 17 00:00:00 2001 From: Max R Date: Sat, 9 Sep 2023 21:54:47 -0400 Subject: [PATCH 838/967] Refactor `target_concurrency` tests --- tests/lang_base_test.py | 62 +++++++++++------------------------------ tests/xargs_test.py | 35 +++++++++++++++++++++++ 2 files changed, 51 insertions(+), 46 deletions(-) diff --git a/tests/lang_base_test.py b/tests/lang_base_test.py index 1cffa0e5f..da289aef8 100644 --- a/tests/lang_base_test.py +++ b/tests/lang_base_test.py @@ -1,6 +1,5 @@ from __future__ import annotations -import multiprocessing import os.path import sys from unittest import mock @@ -10,6 +9,7 @@ import pre_commit.constants as C from pre_commit import lang_base from pre_commit import parse_shebang +from pre_commit import xargs from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError @@ -30,19 +30,6 @@ def fake_expanduser(pth): yield -@pytest.fixture -def no_sched_getaffinity(): - # Simulates an OS without os.sched_getaffinity available (mac/windows) - # https://docs.python.org/3/library/os.html#interface-to-the-scheduler - with mock.patch.object( - os, - 'sched_getaffinity', - create=True, - side_effect=AttributeError, - ): - yield - - def test_exe_exists_does_not_exist(find_exe_mck, homedir_mck): find_exe_mck.return_value = None assert lang_base.exe_exists('ruby') is False @@ -129,40 +116,23 @@ def test_no_env_noop(tmp_path): assert before == inside == after -def test_target_concurrency_sched_getaffinity(no_sched_getaffinity): - with mock.patch.object( - os, - 'sched_getaffinity', - return_value=set(range(345)), - ): - with mock.patch.dict(os.environ, clear=True): - assert lang_base.target_concurrency() == 345 - - -def test_target_concurrency_without_sched_getaffinity(no_sched_getaffinity): - with mock.patch.object(multiprocessing, 'cpu_count', return_value=123): - with mock.patch.dict(os.environ, {}, clear=True): - assert lang_base.target_concurrency() == 123 - - -def test_target_concurrency_testing_env_var(): - with mock.patch.dict( - os.environ, {'PRE_COMMIT_NO_CONCURRENCY': '1'}, clear=True, - ): - assert lang_base.target_concurrency() == 1 - - -def test_target_concurrency_on_travis(): - with mock.patch.dict(os.environ, {'TRAVIS': '1'}, clear=True): - assert lang_base.target_concurrency() == 2 +@pytest.fixture +def cpu_count_mck(): + with mock.patch.object(xargs, 'cpu_count', return_value=4): + yield -def test_target_concurrency_cpu_count_not_implemented(no_sched_getaffinity): - with mock.patch.object( - multiprocessing, 'cpu_count', side_effect=NotImplementedError, - ): - with mock.patch.dict(os.environ, {}, clear=True): - assert lang_base.target_concurrency() == 1 +@pytest.mark.parametrize( + ('var', 'expected'), + ( + ('PRE_COMMIT_NO_CONCURRENCY', 1), + ('TRAVIS', 2), + (None, 4), + ), +) +def test_target_concurrency(cpu_count_mck, var, expected): + with mock.patch.dict(os.environ, {var: '1'} if var else {}, clear=True): + assert lang_base.target_concurrency() == expected def test_shuffled_is_deterministic(): diff --git a/tests/xargs_test.py b/tests/xargs_test.py index b0a8e0d66..e8000b252 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -1,6 +1,7 @@ from __future__ import annotations import concurrent.futures +import multiprocessing import os import sys import time @@ -12,6 +13,40 @@ from pre_commit import xargs +def test_cpu_count_sched_getaffinity_exists(): + with mock.patch.object( + os, 'sched_getaffinity', create=True, return_value=set(range(345)), + ): + assert xargs.cpu_count() == 345 + + +@pytest.fixture +def no_sched_getaffinity(): + # Simulates an OS without os.sched_getaffinity available (mac/windows) + # https://docs.python.org/3/library/os.html#interface-to-the-scheduler + with mock.patch.object( + os, + 'sched_getaffinity', + create=True, + side_effect=AttributeError, + ): + yield + + +def test_cpu_count_multiprocessing_cpu_count_implemented(no_sched_getaffinity): + with mock.patch.object(multiprocessing, 'cpu_count', return_value=123): + assert xargs.cpu_count() == 123 + + +def test_cpu_count_multiprocessing_cpu_count_not_implemented( + no_sched_getaffinity, +): + with mock.patch.object( + multiprocessing, 'cpu_count', side_effect=NotImplementedError, + ): + assert xargs.cpu_count() == 1 + + @pytest.mark.parametrize( ('env', 'expected'), ( From 5d692d7e06606ec34ef3a6acf4a0fa7fef158983 Mon Sep 17 00:00:00 2001 From: Max R Date: Sat, 9 Sep 2023 21:51:59 -0400 Subject: [PATCH 839/967] Short-circuit hooks --- pre_commit/commands/run.py | 50 +++++++++---------- pre_commit/meta_hooks/check_hooks_apply.py | 2 +- .../meta_hooks/check_useless_excludes.py | 14 +++--- tests/commands/run_test.py | 16 +++--- 4 files changed, 42 insertions(+), 40 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index c867799e8..38d80db3a 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -10,7 +10,8 @@ import time import unicodedata from typing import Any -from typing import Collection +from typing import Generator +from typing import Iterable from typing import MutableMapping from typing import Sequence @@ -57,20 +58,20 @@ def _full_msg( def filter_by_include_exclude( - names: Collection[str], + names: Iterable[str], include: str, exclude: str, -) -> list[str]: +) -> Generator[str, None, None]: include_re, exclude_re = re.compile(include), re.compile(exclude) - return [ + return ( filename for filename in names if include_re.search(filename) if not exclude_re.search(filename) - ] + ) class Classifier: - def __init__(self, filenames: Collection[str]) -> None: + def __init__(self, filenames: Iterable[str]) -> None: self.filenames = [f for f in filenames if os.path.lexists(f)] @functools.lru_cache(maxsize=None) @@ -79,15 +80,14 @@ def _types_for_file(self, filename: str) -> set[str]: def by_types( self, - names: Sequence[str], - types: Collection[str], - types_or: Collection[str], - exclude_types: Collection[str], - ) -> list[str]: + names: Iterable[str], + types: Iterable[str], + types_or: Iterable[str], + exclude_types: Iterable[str], + ) -> Generator[str, None, None]: types = frozenset(types) types_or = frozenset(types_or) exclude_types = frozenset(exclude_types) - ret = [] for filename in names: tags = self._types_for_file(filename) if ( @@ -95,24 +95,24 @@ def by_types( (not types_or or tags & types_or) and not tags & exclude_types ): - ret.append(filename) - return ret - - def filenames_for_hook(self, hook: Hook) -> tuple[str, ...]: - names = self.filenames - names = filter_by_include_exclude(names, hook.files, hook.exclude) - names = self.by_types( - names, + yield filename + + def filenames_for_hook(self, hook: Hook) -> Generator[str, None, None]: + return self.by_types( + filter_by_include_exclude( + self.filenames, + hook.files, + hook.exclude, + ), hook.types, hook.types_or, hook.exclude_types, ) - return tuple(names) @classmethod def from_config( cls, - filenames: Collection[str], + filenames: Iterable[str], include: str, exclude: str, ) -> Classifier: @@ -121,7 +121,7 @@ def from_config( # this also makes improperly quoted shell-based hooks work better # see #1173 if os.altsep == '/' and os.sep == '\\': - filenames = [f.replace(os.sep, os.altsep) for f in filenames] + filenames = (f.replace(os.sep, os.altsep) for f in filenames) filenames = filter_by_include_exclude(filenames, include, exclude) return Classifier(filenames) @@ -148,7 +148,7 @@ def _run_single_hook( verbose: bool, use_color: bool, ) -> tuple[bool, bytes]: - filenames = classifier.filenames_for_hook(hook) + filenames = tuple(classifier.filenames_for_hook(hook)) if hook.id in skips or hook.alias in skips: output.write( @@ -250,7 +250,7 @@ def _compute_cols(hooks: Sequence[Hook]) -> int: return max(cols, 80) -def _all_filenames(args: argparse.Namespace) -> Collection[str]: +def _all_filenames(args: argparse.Namespace) -> Iterable[str]: # these hooks do not operate on files if args.hook_stage in { 'post-checkout', 'post-commit', 'post-merge', 'post-rewrite', diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index b05a70500..7f491a209 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -21,7 +21,7 @@ def check_all_hooks_match_files(config_file: str) -> int: for hook in all_hooks(config, Store()): if hook.always_run or hook.language == 'fail': continue - elif not classifier.filenames_for_hook(hook): + elif not any(classifier.filenames_for_hook(hook)): print(f'{hook.id} does not apply to this repository') retv = 1 diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 0a8249b85..8b0c106a3 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -2,6 +2,7 @@ import argparse import re +from typing import Iterable from typing import Sequence from cfgv import apply_defaults @@ -14,7 +15,7 @@ def exclude_matches_any( - filenames: Sequence[str], + filenames: Iterable[str], include: str, exclude: str, ) -> bool: @@ -50,11 +51,12 @@ def check_useless_excludes(config_file: str) -> int: # Not actually a manifest dict, but this more accurately reflects # the defaults applied during runtime hook = apply_defaults(hook, MANIFEST_HOOK_DICT) - names = classifier.filenames - types = hook['types'] - types_or = hook['types_or'] - exclude_types = hook['exclude_types'] - names = classifier.by_types(names, types, types_or, exclude_types) + names = classifier.by_types( + classifier.filenames, + hook['types'], + hook['types_or'], + hook['exclude_types'], + ) include, exclude = hook['files'], hook['exclude'] if not exclude_matches_any(names, include, exclude): print( diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index dd15b94c5..8d89815b5 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -1127,8 +1127,8 @@ def test_classifier_empty_types_or(tmpdir): types_or=[], exclude_types=[], ) - assert for_symlink == ['foo'] - assert for_file == ['bar'] + assert tuple(for_symlink) == ('foo',) + assert tuple(for_file) == ('bar',) @pytest.fixture @@ -1142,33 +1142,33 @@ def some_filenames(): def test_include_exclude_base_case(some_filenames): ret = filter_by_include_exclude(some_filenames, '', '^$') - assert ret == [ + assert tuple(ret) == ( '.pre-commit-hooks.yaml', 'pre_commit/git.py', 'pre_commit/main.py', - ] + ) def test_matches_broken_symlink(tmpdir): with tmpdir.as_cwd(): os.symlink('does-not-exist', 'link') ret = filter_by_include_exclude({'link'}, '', '^$') - assert ret == ['link'] + assert tuple(ret) == ('link',) def test_include_exclude_total_match(some_filenames): ret = filter_by_include_exclude(some_filenames, r'^.*\.py$', '^$') - assert ret == ['pre_commit/git.py', 'pre_commit/main.py'] + assert tuple(ret) == ('pre_commit/git.py', 'pre_commit/main.py') def test_include_exclude_does_search_instead_of_match(some_filenames): ret = filter_by_include_exclude(some_filenames, r'\.yaml$', '^$') - assert ret == ['.pre-commit-hooks.yaml'] + assert tuple(ret) == ('.pre-commit-hooks.yaml',) def test_include_exclude_exclude_removes_files(some_filenames): ret = filter_by_include_exclude(some_filenames, '', r'\.py$') - assert ret == ['.pre-commit-hooks.yaml'] + assert tuple(ret) == ('.pre-commit-hooks.yaml',) def test_args_hook_only(cap_out, store, repo_with_passing_hook): From d33801e78176d91023a441ca869ecb0288f4f461 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 19 Sep 2023 07:06:08 +0000 Subject: [PATCH 840/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder-python-imports: v3.10.0 → v3.11.0](https://github.com/asottile/reorder-python-imports/compare/v3.10.0...v3.11.0) - [github.com/asottile/pyupgrade: v3.10.1 → v3.11.0](https://github.com/asottile/pyupgrade/compare/v3.10.1...v3.11.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3b98f96bc..fb969280c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports - rev: v3.10.0 + rev: v3.11.0 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.10.1 + rev: v3.11.0 hooks: - id: pyupgrade args: [--py38-plus] From 5e05b012157763a46f5f0364ce575d7b99cf5c21 Mon Sep 17 00:00:00 2001 From: Eric Long Date: Mon, 25 Sep 2023 17:00:29 +0800 Subject: [PATCH 841/967] Bump Node.js version to 18.14.0 and Go to 1.21.1 On riscv64, nodeenv will pull binary from unofficial-builds [1], and unfortunately 18.13.0 seems to be the only version above 18 that is missing riscv64 builds. Shifting the version slightly to make test work. Go's binary now ships with linux/riscv64 binary since 1.21. --- tests/languages/golang_test.py | 4 ++-- tests/languages/node_test.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py index 640626711..19e9f62f6 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -111,11 +111,11 @@ def test_golang_versioned(tmp_path): tmp_path, golang, 'go version', - version='1.18.4', + version='1.21.1', ) assert ret == 0 - assert out.startswith(b'go version go1.18.4') + assert out.startswith(b'go version go1.21.1') def test_local_golang_additional_deps(tmp_path): diff --git a/tests/languages/node_test.py b/tests/languages/node_test.py index cba0228b3..055cb1e92 100644 --- a/tests/languages/node_test.py +++ b/tests/languages/node_test.py @@ -139,7 +139,7 @@ def test_node_with_user_config_set(tmp_path): test_node_hook_system(tmp_path) -@pytest.mark.parametrize('version', (C.DEFAULT, '18.13.0')) +@pytest.mark.parametrize('version', (C.DEFAULT, '18.14.0')) def test_node_hook_versions(tmp_path, version): _make_hello_world(tmp_path) ret = run_language(tmp_path, node, 'node-hello', version=version) From c68c6b944aafed8d7b7d75c0d6a0f4109d7dde50 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 26 Sep 2023 07:23:52 +0000 Subject: [PATCH 842/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.11.0 → v3.13.0](https://github.com/asottile/pyupgrade/compare/v3.11.0...v3.13.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fb969280c..309e9de5a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.11.0 + rev: v3.13.0 hooks: - id: pyupgrade args: [--py38-plus] From a4ab977cc36e06fff8a8c69cca652162407b55cf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 3 Oct 2023 08:58:04 +0000 Subject: [PATCH 843/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v2.4.0 → v2.5.0](https://github.com/asottile/setup-cfg-fmt/compare/v2.4.0...v2.5.0) - [github.com/asottile/reorder-python-imports: v3.11.0 → v3.12.0](https://github.com/asottile/reorder-python-imports/compare/v3.11.0...v3.12.0) - [github.com/asottile/pyupgrade: v3.13.0 → v3.14.0](https://github.com/asottile/pyupgrade/compare/v3.13.0...v3.14.0) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 309e9de5a..cccecb8e2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,11 +10,11 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.4.0 + rev: v2.5.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports - rev: v3.11.0 + rev: v3.12.0 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.13.0 + rev: v3.14.0 hooks: - id: pyupgrade args: [--py38-plus] From 997ea0ad52074c3e6474f3d99f76f7965e2d05f0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 9 Oct 2023 16:49:30 -0400 Subject: [PATCH 844/967] use sys.executable instead of echo.exe in parse_shebang the GHA runners now have echo.exe in a path with spaces --- tests/parse_shebang_test.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index dd97ca5d8..bd4384df2 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -133,17 +133,17 @@ def test_normalize_cmd_PATH(): def test_normalize_cmd_shebang(in_tmpdir): - echo = _echo_exe().replace(os.sep, '/') - path = write_executable(echo) - assert parse_shebang.normalize_cmd((path,)) == (echo, path) + us = sys.executable.replace(os.sep, '/') + path = write_executable(us) + assert parse_shebang.normalize_cmd((path,)) == (us, path) def test_normalize_cmd_PATH_shebang_full_path(in_tmpdir): - echo = _echo_exe().replace(os.sep, '/') - path = write_executable(echo) + us = sys.executable.replace(os.sep, '/') + path = write_executable(us) with bin_on_path(): ret = parse_shebang.normalize_cmd(('run',)) - assert ret == (echo, os.path.abspath(path)) + assert ret == (us, os.path.abspath(path)) def test_normalize_cmd_PATH_shebang_PATH(in_tmpdir): From 155c52134848b05b0092a446cdd2c336a03a85c0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 Oct 2023 20:33:33 +0000 Subject: [PATCH 845/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.4.0 → v4.5.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.4.0...v4.5.0) - [github.com/asottile/pyupgrade: v3.14.0 → v3.15.0](https://github.com/asottile/pyupgrade/compare/v3.14.0...v3.15.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cccecb8e2..5381cd611 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.14.0 + rev: v3.15.0 hooks: - id: pyupgrade args: [--py38-plus] From d988767b414495bdab9ea24532ad337e8ee3fd1f Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Fri, 13 Oct 2023 16:01:59 +0100 Subject: [PATCH 846/967] Improve hook duration timing --- pre_commit/commands/run.py | 4 ++-- tests/commands/run_test.py | 2 +- tests/commands/try_repo_test.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index c867799e8..241f6fe16 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -187,7 +187,7 @@ def _run_single_hook( if not hook.pass_filenames: filenames = () - time_before = time.time() + time_before = time.monotonic() language = languages[hook.language] with language.in_env(hook.prefix, hook.language_version): retcode, out = language.run_hook( @@ -199,7 +199,7 @@ def _run_single_hook( require_serial=hook.require_serial, color=use_color, ) - duration = round(time.time() - time_before, 2) or 0 + duration = round(time.monotonic() - time_before, 2) or 0 diff_after = _get_diff() # if the hook makes changes, fail the commit diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index dd15b94c5..4be8f3b9e 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -293,7 +293,7 @@ def test_verbose_duration(cap_out, store, in_git_dir, t1, t2, expected): write_config('.', {'repo': 'meta', 'hooks': [{'id': 'identity'}]}) cmd_output('git', 'add', '.') opts = run_opts(verbose=True) - with mock.patch.object(time, 'time', side_effect=(t1, t2)): + with mock.patch.object(time, 'monotonic', side_effect=(t1, t2)): ret, printed = _do_run(cap_out, store, str(in_git_dir), opts) assert ret == 0 assert expected in printed diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index 0b2db7e5a..c5f891ea7 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -43,7 +43,7 @@ def _run_try_repo(tempdir_factory, **kwargs): def test_try_repo_repo_only(cap_out, tempdir_factory): - with mock.patch.object(time, 'time', return_value=0.0): + with mock.patch.object(time, 'monotonic', return_value=0.0): _run_try_repo(tempdir_factory, verbose=True) start, config, rest = _get_out(cap_out) assert start == '' From 61cc55a59cc63c7405dd3cd7c96b169fdb750333 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 13 Oct 2023 11:57:20 -0400 Subject: [PATCH 847/967] v3.5.0 --- CHANGELOG.md | 17 +++++++++++++++++ setup.cfg | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e2ef0de1..7a1b61a49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +3.5.0 - 2023-10-13 +================== + +### Features +- Improve performance of `check-hooks-apply` and `check-useless-excludes`. + - #2998 PR by @mxr. + - #2935 issue by @mxr. + +### Fixes +- Use `time.monotonic()` for more accurate hook timing. + - #3024 PR by @adamchainz. + +### Migrating +- Require npm 6.x+ for `language: node` hooks. + - #2996 PR by @RoelAdriaans. + - #1983 issue by @henryiii. + 3.4.0 - 2023-09-02 ================== diff --git a/setup.cfg b/setup.cfg index cfaa61bbb..7543835d7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.4.0 +version = 3.5.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 44b625ebd3c3f239737ee1ea0603daffbd61c4e2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 20:03:36 +0000 Subject: [PATCH 848/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.5.1 → v1.6.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.5.1...v1.6.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5381cd611..0ef18ba32 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.5.1 + rev: v1.6.0 hooks: - id: mypy additional_dependencies: [types-all] From c69e32e925dc4ef160aa9ecde13bea73f2175803 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 20:28:04 +0000 Subject: [PATCH 849/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.6.0 → v1.6.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.6.0...v1.6.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0ef18ba32..46dce4813 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.6.0 + rev: v1.6.1 hooks: - id: mypy additional_dependencies: [types-all] From 7f15dc75eea8ad1017c9870e1468d6a9e5339ac3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Oct 2023 14:20:37 -0400 Subject: [PATCH 850/967] python3.9+ --- .github/actions/pre-test/action.yml | 2 +- .github/workflows/languages.yaml | 4 ++-- .github/workflows/main.yml | 8 ++++---- .pre-commit-config.yaml | 4 ++-- pre_commit/clientlib.py | 2 +- pre_commit/commands/autoupdate.py | 2 +- pre_commit/commands/hook_impl.py | 2 +- pre_commit/commands/run.py | 10 +++++----- pre_commit/commands/validate_config.py | 2 +- pre_commit/commands/validate_manifest.py | 2 +- pre_commit/envcontext.py | 9 ++++----- pre_commit/error_handler.py | 2 +- pre_commit/file_lock.py | 2 +- pre_commit/git.py | 2 +- pre_commit/hook.py | 2 +- pre_commit/lang_base.py | 4 ++-- pre_commit/languages/conda.py | 4 ++-- pre_commit/languages/coursier.py | 4 ++-- pre_commit/languages/dart.py | 4 ++-- pre_commit/languages/docker.py | 2 +- pre_commit/languages/docker_image.py | 2 +- pre_commit/languages/dotnet.py | 4 ++-- pre_commit/languages/fail.py | 2 +- pre_commit/languages/golang.py | 4 ++-- pre_commit/languages/haskell.py | 4 ++-- pre_commit/languages/lua.py | 4 ++-- pre_commit/languages/node.py | 4 ++-- pre_commit/languages/perl.py | 4 ++-- pre_commit/languages/pygrep.py | 4 ++-- pre_commit/languages/python.py | 6 +++--- pre_commit/languages/r.py | 4 ++-- pre_commit/languages/ruby.py | 4 ++-- pre_commit/languages/rust.py | 4 ++-- pre_commit/languages/script.py | 2 +- pre_commit/languages/swift.py | 4 ++-- pre_commit/logging_handler.py | 2 +- pre_commit/main.py | 2 +- pre_commit/meta_hooks/check_hooks_apply.py | 2 +- pre_commit/meta_hooks/check_useless_excludes.py | 4 ++-- pre_commit/meta_hooks/identity.py | 2 +- pre_commit/parse_shebang.py | 2 +- pre_commit/repository.py | 2 +- pre_commit/staged_files_only.py | 2 +- pre_commit/store.py | 4 ++-- pre_commit/util.py | 2 +- pre_commit/xargs.py | 8 ++++---- setup.cfg | 2 +- testing/language_helpers.py | 2 +- testing/make-archives | 2 +- tests/commands/run_test.py | 2 +- 50 files changed, 84 insertions(+), 85 deletions(-) diff --git a/.github/actions/pre-test/action.yml b/.github/actions/pre-test/action.yml index 9d1eb2de6..b70c942fe 100644 --- a/.github/actions/pre-test/action.yml +++ b/.github/actions/pre-test/action.yml @@ -6,4 +6,4 @@ runs: using: composite steps: - uses: asottile/workflows/.github/actions/latest-git@v1.4.0 - if: inputs.env == 'py38' && runner.os == 'Linux' + if: inputs.env == 'py39' && runner.os == 'Linux' diff --git a/.github/workflows/languages.yaml b/.github/workflows/languages.yaml index 5a6ae9cd7..7d50535f8 100644 --- a/.github/workflows/languages.yaml +++ b/.github/workflows/languages.yaml @@ -21,7 +21,7 @@ jobs: fetch-depth: 0 - uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: 3.9 - name: install deps run: python -mpip install -e . -r requirements-dev.txt - name: vars @@ -39,7 +39,7 @@ jobs: - uses: asottile/workflows/.github/actions/fast-checkout@v1.4.0 - uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: 3.9 - run: echo "$CONDA\Scripts" >> "$GITHUB_PATH" shell: bash diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 903d24780..6e32f6c6b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,12 +12,12 @@ concurrency: jobs: main-windows: - uses: asottile/workflows/.github/workflows/tox.yml@v1.4.0 + uses: asottile/workflows/.github/workflows/tox.yml@v1.6.0 with: - env: '["py38"]' + env: '["py39"]' os: windows-latest main-linux: - uses: asottile/workflows/.github/workflows/tox.yml@v1.4.0 + uses: asottile/workflows/.github/workflows/tox.yml@v1.6.0 with: - env: '["py38", "py39", "py310"]' + env: '["py39", "py310", "py311"]' os: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5381cd611..ca2dc42b2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) - args: [--py38-plus, --add-import, 'from __future__ import annotations'] + args: [--py39-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma rev: v3.1.0 hooks: @@ -27,7 +27,7 @@ repos: rev: v3.15.0 hooks: - id: pyupgrade - args: [--py38-plus] + args: [--py39-plus] - repo: https://github.com/hhatto/autopep8 rev: v2.0.4 hooks: diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index d0651cae2..9f41bf4b2 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -5,9 +5,9 @@ import re import shlex import sys +from collections.abc import Sequence from typing import Any from typing import NamedTuple -from typing import Sequence import cfgv from identify.identify import ALL_TAGS diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index e7725fdc4..aa0c5e25e 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -4,9 +4,9 @@ import os.path import re import tempfile +from collections.abc import Sequence from typing import Any from typing import NamedTuple -from typing import Sequence import pre_commit.constants as C from pre_commit import git diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index dab2135d4..49a80b7b3 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -4,7 +4,7 @@ import os.path import subprocess import sys -from typing import Sequence +from collections.abc import Sequence from pre_commit.commands.run import run from pre_commit.envcontext import envcontext diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 41ba4ecf0..076f16d8f 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -9,11 +9,11 @@ import subprocess import time import unicodedata +from collections.abc import Generator +from collections.abc import Iterable +from collections.abc import MutableMapping +from collections.abc import Sequence from typing import Any -from typing import Generator -from typing import Iterable -from typing import MutableMapping -from typing import Sequence from identify.identify import tags_from_path @@ -74,7 +74,7 @@ class Classifier: def __init__(self, filenames: Iterable[str]) -> None: self.filenames = [f for f in filenames if os.path.lexists(f)] - @functools.lru_cache(maxsize=None) + @functools.cache def _types_for_file(self, filename: str) -> set[str]: return tags_from_path(filename) diff --git a/pre_commit/commands/validate_config.py b/pre_commit/commands/validate_config.py index 24bd3135e..b3de635b1 100644 --- a/pre_commit/commands/validate_config.py +++ b/pre_commit/commands/validate_config.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Sequence +from collections.abc import Sequence from pre_commit import clientlib diff --git a/pre_commit/commands/validate_manifest.py b/pre_commit/commands/validate_manifest.py index 419031a9b..8493c6e1e 100644 --- a/pre_commit/commands/validate_manifest.py +++ b/pre_commit/commands/validate_manifest.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Sequence +from collections.abc import Sequence from pre_commit import clientlib diff --git a/pre_commit/envcontext.py b/pre_commit/envcontext.py index 4f5956016..1f816cea9 100644 --- a/pre_commit/envcontext.py +++ b/pre_commit/envcontext.py @@ -3,10 +3,9 @@ import contextlib import enum import os -from typing import Generator -from typing import MutableMapping +from collections.abc import Generator +from collections.abc import MutableMapping from typing import NamedTuple -from typing import Tuple from typing import Union _Unset = enum.Enum('_Unset', 'UNSET') @@ -18,9 +17,9 @@ class Var(NamedTuple): default: str = '' -SubstitutionT = Tuple[Union[str, Var], ...] +SubstitutionT = tuple[Union[str, Var], ...] ValueT = Union[str, _Unset, SubstitutionT] -PatchesT = Tuple[Tuple[str, ValueT], ...] +PatchesT = tuple[tuple[str, ValueT], ...] def format_env(parts: SubstitutionT, env: MutableMapping[str, str]) -> str: diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index d740ee3e4..73e608b71 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -5,7 +5,7 @@ import os.path import sys import traceback -from typing import Generator +from collections.abc import Generator from typing import IO import pre_commit.constants as C diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index f67a58644..d3dafb4da 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -3,8 +3,8 @@ import contextlib import errno import sys +from collections.abc import Generator from typing import Callable -from typing import Generator if sys.platform == 'win32': # pragma: no cover (windows) diff --git a/pre_commit/git.py b/pre_commit/git.py index 333dc7ba3..19aac3872 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -3,7 +3,7 @@ import logging import os.path import sys -from typing import Mapping +from collections.abc import Mapping from pre_commit.errors import FatalError from pre_commit.util import CalledProcessError diff --git a/pre_commit/hook.py b/pre_commit/hook.py index 6d436ca30..309cd5be3 100644 --- a/pre_commit/hook.py +++ b/pre_commit/hook.py @@ -1,9 +1,9 @@ from __future__ import annotations import logging +from collections.abc import Sequence from typing import Any from typing import NamedTuple -from typing import Sequence from pre_commit.prefix import Prefix diff --git a/pre_commit/lang_base.py b/pre_commit/lang_base.py index 4a993eaa4..5303948b5 100644 --- a/pre_commit/lang_base.py +++ b/pre_commit/lang_base.py @@ -5,12 +5,12 @@ import random import re import shlex +from collections.abc import Generator +from collections.abc import Sequence from typing import Any from typing import ContextManager -from typing import Generator from typing import NoReturn from typing import Protocol -from typing import Sequence import pre_commit.constants as C from pre_commit import parse_shebang diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index 41c355e77..80b3e1507 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -3,8 +3,8 @@ import contextlib import os import sys -from typing import Generator -from typing import Sequence +from collections.abc import Generator +from collections.abc import Sequence from pre_commit import lang_base from pre_commit.envcontext import envcontext diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index 9c5fbfe24..6558bf6b8 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -2,8 +2,8 @@ import contextlib import os.path -from typing import Generator -from typing import Sequence +from collections.abc import Generator +from collections.abc import Sequence from pre_commit import lang_base from pre_commit.envcontext import envcontext diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py index e8539caa2..129ac5918 100644 --- a/pre_commit/languages/dart.py +++ b/pre_commit/languages/dart.py @@ -4,8 +4,8 @@ import os.path import shutil import tempfile -from typing import Generator -from typing import Sequence +from collections.abc import Generator +from collections.abc import Sequence from pre_commit import lang_base from pre_commit.envcontext import envcontext diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 8e53ca9e3..26328515e 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -3,7 +3,7 @@ import hashlib import json import os -from typing import Sequence +from collections.abc import Sequence from pre_commit import lang_base from pre_commit.prefix import Prefix diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index 26f006e4a..a1a2c169a 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Sequence +from collections.abc import Sequence from pre_commit import lang_base from pre_commit.languages.docker import docker_cmd diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index e9568f222..e1202c4f2 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -6,8 +6,8 @@ import tempfile import xml.etree.ElementTree import zipfile -from typing import Generator -from typing import Sequence +from collections.abc import Generator +from collections.abc import Sequence from pre_commit import lang_base from pre_commit.envcontext import envcontext diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index a8ec6a53d..6ac4d7675 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Sequence +from collections.abc import Sequence from pre_commit import lang_base from pre_commit.prefix import Prefix diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index bea91e9bd..4c13d8f9c 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -12,11 +12,11 @@ import urllib.error import urllib.request import zipfile +from collections.abc import Generator +from collections.abc import Sequence from typing import ContextManager -from typing import Generator from typing import IO from typing import Protocol -from typing import Sequence import pre_commit.constants as C from pre_commit import lang_base diff --git a/pre_commit/languages/haskell.py b/pre_commit/languages/haskell.py index 76442eb02..c6945c822 100644 --- a/pre_commit/languages/haskell.py +++ b/pre_commit/languages/haskell.py @@ -2,8 +2,8 @@ import contextlib import os.path -from typing import Generator -from typing import Sequence +from collections.abc import Generator +from collections.abc import Sequence from pre_commit import lang_base from pre_commit.envcontext import envcontext diff --git a/pre_commit/languages/lua.py b/pre_commit/languages/lua.py index 12d066140..a475ec99c 100644 --- a/pre_commit/languages/lua.py +++ b/pre_commit/languages/lua.py @@ -3,8 +3,8 @@ import contextlib import os import sys -from typing import Generator -from typing import Sequence +from collections.abc import Generator +from collections.abc import Sequence from pre_commit import lang_base from pre_commit.envcontext import envcontext diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 3e22dc78e..d49c0e326 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -4,8 +4,8 @@ import functools import os import sys -from typing import Generator -from typing import Sequence +from collections.abc import Generator +from collections.abc import Sequence import pre_commit.constants as C from pre_commit import lang_base diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py index 2a7f16290..61b1d114b 100644 --- a/pre_commit/languages/perl.py +++ b/pre_commit/languages/perl.py @@ -3,8 +3,8 @@ import contextlib import os import shlex -from typing import Generator -from typing import Sequence +from collections.abc import Generator +from collections.abc import Sequence from pre_commit import lang_base from pre_commit.envcontext import envcontext diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index ec55560b0..72a9345fa 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -3,9 +3,9 @@ import argparse import re import sys +from collections.abc import Sequence +from re import Pattern from typing import NamedTuple -from typing import Pattern -from typing import Sequence from pre_commit import lang_base from pre_commit import output diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 3ef343608..e5bac9fa8 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -4,8 +4,8 @@ import functools import os import sys -from typing import Generator -from typing import Sequence +from collections.abc import Generator +from collections.abc import Sequence import pre_commit.constants as C from pre_commit import lang_base @@ -24,7 +24,7 @@ run_hook = lang_base.basic_run_hook -@functools.lru_cache(maxsize=None) +@functools.cache def _version_info(exe: str) -> str: prog = 'import sys;print(".".join(str(p) for p in sys.version_info))' try: diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 6feb06523..93b62bd53 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -6,8 +6,8 @@ import shutil import tempfile import textwrap -from typing import Generator -from typing import Sequence +from collections.abc import Generator +from collections.abc import Sequence from pre_commit import lang_base from pre_commit.envcontext import envcontext diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index c88269f24..3ed15cfcc 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -6,9 +6,9 @@ import os.path import shutil import tarfile -from typing import Generator +from collections.abc import Generator +from collections.abc import Sequence from typing import IO -from typing import Sequence import pre_commit.constants as C from pre_commit import lang_base diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 7eec0e7d6..241146c57 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -7,8 +7,8 @@ import sys import tempfile import urllib.request -from typing import Generator -from typing import Sequence +from collections.abc import Generator +from collections.abc import Sequence import pre_commit.constants as C from pre_commit import lang_base diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 89a3ab2d6..1eaa1e270 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Sequence +from collections.abc import Sequence from pre_commit import lang_base from pre_commit.prefix import Prefix diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index f16bb0451..f7bfe84c5 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -2,8 +2,8 @@ import contextlib import os -from typing import Generator -from typing import Sequence +from collections.abc import Generator +from collections.abc import Sequence from pre_commit import lang_base from pre_commit.envcontext import envcontext diff --git a/pre_commit/logging_handler.py b/pre_commit/logging_handler.py index 1b68fc7d6..cd33953d7 100644 --- a/pre_commit/logging_handler.py +++ b/pre_commit/logging_handler.py @@ -2,7 +2,7 @@ import contextlib import logging -from typing import Generator +from collections.abc import Generator from pre_commit import color from pre_commit import output diff --git a/pre_commit/main.py b/pre_commit/main.py index 9dfce2c25..18c978a84 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -4,7 +4,7 @@ import logging import os import sys -from typing import Sequence +from collections.abc import Sequence import pre_commit.constants as C from pre_commit import clientlib diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index 7f491a209..84c142b45 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -1,7 +1,7 @@ from __future__ import annotations import argparse -from typing import Sequence +from collections.abc import Sequence import pre_commit.constants as C from pre_commit import git diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 8b0c106a3..664251a44 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -2,8 +2,8 @@ import argparse import re -from typing import Iterable -from typing import Sequence +from collections.abc import Iterable +from collections.abc import Sequence from cfgv import apply_defaults diff --git a/pre_commit/meta_hooks/identity.py b/pre_commit/meta_hooks/identity.py index 72ee440bc..3e20bbc68 100644 --- a/pre_commit/meta_hooks/identity.py +++ b/pre_commit/meta_hooks/identity.py @@ -1,7 +1,7 @@ from __future__ import annotations import sys -from typing import Sequence +from collections.abc import Sequence from pre_commit import output diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index 3ee04e8d7..043a9b5d7 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -1,7 +1,7 @@ from __future__ import annotations import os.path -from typing import Mapping +from collections.abc import Mapping from typing import NoReturn from identify.identify import parse_shebang_from_file diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 040f238f0..439a09b4f 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -4,8 +4,8 @@ import logging import os import shlex +from collections.abc import Sequence from typing import Any -from typing import Sequence import pre_commit.constants as C from pre_commit.all_languages import languages diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 881235656..fd28e1c22 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -4,7 +4,7 @@ import logging import os.path import time -from typing import Generator +from collections.abc import Generator from pre_commit import git from pre_commit.errors import FatalError diff --git a/pre_commit/store.py b/pre_commit/store.py index 487e3e798..84bc09a4c 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -5,9 +5,9 @@ import os.path import sqlite3 import tempfile +from collections.abc import Generator +from collections.abc import Sequence from typing import Callable -from typing import Generator -from typing import Sequence import pre_commit.constants as C from pre_commit import file_lock diff --git a/pre_commit/util.py b/pre_commit/util.py index 4f8e8357d..1e3112693 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -8,10 +8,10 @@ import stat import subprocess import sys +from collections.abc import Generator from types import TracebackType from typing import Any from typing import Callable -from typing import Generator from pre_commit import parse_shebang diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index a7493c01d..22580f595 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -7,12 +7,12 @@ import os import subprocess import sys +from collections.abc import Generator +from collections.abc import Iterable +from collections.abc import MutableMapping +from collections.abc import Sequence from typing import Any from typing import Callable -from typing import Generator -from typing import Iterable -from typing import MutableMapping -from typing import Sequence from typing import TypeVar from pre_commit import parse_shebang diff --git a/setup.cfg b/setup.cfg index 7543835d7..3110881fc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,7 +24,7 @@ install_requires = nodeenv>=0.11.1 pyyaml>=5.1 virtualenv>=20.10.0 -python_requires = >=3.8 +python_requires = >=3.9 [options.packages.find] exclude = diff --git a/testing/language_helpers.py b/testing/language_helpers.py index ead8dae27..05c94ebca 100644 --- a/testing/language_helpers.py +++ b/testing/language_helpers.py @@ -1,7 +1,7 @@ from __future__ import annotations import os -from typing import Sequence +from collections.abc import Sequence from pre_commit.lang_base import Language from pre_commit.prefix import Prefix diff --git a/testing/make-archives b/testing/make-archives index 8ec05e2de..3c7ab9dd0 100755 --- a/testing/make-archives +++ b/testing/make-archives @@ -8,7 +8,7 @@ import shutil import subprocess import tarfile import tempfile -from typing import Sequence +from collections.abc import Sequence # This is a script for generating the tarred resources for git repo diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 6a0cd8556..e36a3ca9c 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -4,7 +4,7 @@ import shlex import sys import time -from typing import MutableMapping +from collections.abc import MutableMapping from unittest import mock import pytest From 75f2710bd4ffdce232fd1a37e9accbcac3ade14a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Oct 2023 14:39:49 -0400 Subject: [PATCH 851/967] 3.13 removed the simpler importlib.resources api --- pre_commit/languages/ruby.py | 3 ++- pre_commit/util.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 3ed15cfcc..0438ae095 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -25,7 +25,8 @@ def _resource_bytesio(filename: str) -> IO[bytes]: - return importlib.resources.open_binary('pre_commit.resources', filename) + files = importlib.resources.files('pre_commit.resources') + return files.joinpath(filename).open('rb') @functools.lru_cache(maxsize=1) diff --git a/pre_commit/util.py b/pre_commit/util.py index 1e3112693..8f5958414 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -36,7 +36,8 @@ def clean_path_on_failure(path: str) -> Generator[None, None, None]: def resource_text(filename: str) -> str: - return importlib.resources.read_text('pre_commit.resources', filename) + files = importlib.resources.files('pre_commit.resources') + return files.joinpath(filename).read_text() def make_executable(filename: str) -> None: From 1d474994e0d4276c98f5ab22f7f84e5570318f8d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 20:35:35 +0000 Subject: [PATCH 852/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.6.1 → v1.7.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.6.1...v1.7.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 858be1bac..5547ec1f0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.6.1 + rev: v1.7.0 hooks: - id: mypy additional_dependencies: [types-all] From e36cefc8bd43aaee1686d16e31ecb98f576fe121 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 Nov 2023 20:01:19 +0000 Subject: [PATCH 853/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.7.0 → v1.7.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.7.0...v1.7.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5547ec1f0..4433e4e2f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.7.0 + rev: v1.7.1 hooks: - id: mypy additional_dependencies: [types-all] From cffabe54be63f0fd05b42ae73842387d07110feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez=20Mondrag=C3=B3n?= Date: Fri, 1 Dec 2023 17:02:12 -0600 Subject: [PATCH 854/967] Address deprecation warning in `shutil.rmtree(onerror=...)` --- .github/workflows/main.yml | 2 +- pre_commit/util.py | 46 ++++++++++++++++++++++++-------------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6e32f6c6b..2355b6620 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,5 +19,5 @@ jobs: main-linux: uses: asottile/workflows/.github/workflows/tox.yml@v1.6.0 with: - env: '["py39", "py310", "py311"]' + env: '["py39", "py310", "py311", "py312"]' os: ubuntu-latest diff --git a/pre_commit/util.py b/pre_commit/util.py index 8f5958414..b3682d4f7 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -202,24 +202,36 @@ def cmd_output_p( cmd_output_p = cmd_output_b -def rmtree(path: str) -> None: - """On windows, rmtree fails for readonly dirs.""" - def handle_remove_readonly( - func: Callable[..., Any], - path: str, - exc: tuple[type[OSError], OSError, TracebackType], +def _handle_readonly( + func: Callable[[str], object], + path: str, + exc: OSError, +) -> None: + if ( + func in (os.rmdir, os.remove, os.unlink) and + exc.errno in {errno.EACCES, errno.EPERM} + ): + for p in (path, os.path.dirname(path)): + os.chmod(p, os.stat(p).st_mode | stat.S_IWUSR) + func(path) + else: + raise + + +if sys.version_info < (3, 12): # pragma: <3.12 cover + def _handle_readonly_old( + func: Callable[[str], object], + path: str, + excinfo: tuple[type[OSError], OSError, TracebackType], ) -> None: - excvalue = exc[1] - if ( - func in (os.rmdir, os.remove, os.unlink) and - excvalue.errno in {errno.EACCES, errno.EPERM} - ): - for p in (path, os.path.dirname(path)): - os.chmod(p, os.stat(p).st_mode | stat.S_IWUSR) - func(path) - else: - raise - shutil.rmtree(path, ignore_errors=False, onerror=handle_remove_readonly) + return _handle_readonly(func, path, excinfo[1]) + + def rmtree(path: str) -> None: + shutil.rmtree(path, ignore_errors=False, onerror=_handle_readonly_old) +else: # pragma: >=3.12 cover + def rmtree(path: str) -> None: + """On windows, rmtree fails for readonly dirs.""" + shutil.rmtree(path, ignore_errors=False, onexc=_handle_readonly) def win_exe(s: str) -> str: From 047439abffb164edd5b49e50439fd63a625be3da Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 9 Dec 2023 15:34:16 -0500 Subject: [PATCH 855/967] attempt minimum_pre_commit_version first when parsing configs --- pre_commit/clientlib.py | 20 ++-- pre_commit/repository.py | 10 -- tests/clientlib_test.py | 195 +++++++++++++++++++++------------------ tests/repository_test.py | 28 ------ 4 files changed, 119 insertions(+), 134 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 9f41bf4b2..a49465e89 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -102,6 +102,13 @@ def apply_default(self, dct: dict[str, Any]) -> None: MANIFEST_HOOK_DICT = cfgv.Map( 'Hook', 'id', + # check first in case it uses some newer, incompatible feature + cfgv.Optional( + 'minimum_pre_commit_version', + cfgv.check_and(cfgv.check_string, check_min_version), + '0', + ), + cfgv.Required('id', cfgv.check_string), cfgv.Required('name', cfgv.check_string), cfgv.Required('entry', cfgv.check_string), @@ -124,7 +131,6 @@ def apply_default(self, dct: dict[str, Any]) -> None: cfgv.Optional('description', cfgv.check_string, ''), cfgv.Optional('language_version', cfgv.check_string, C.DEFAULT), cfgv.Optional('log_file', cfgv.check_string, ''), - cfgv.Optional('minimum_pre_commit_version', cfgv.check_string, '0'), cfgv.Optional('require_serial', cfgv.check_bool, False), StagesMigration('stages', []), cfgv.Optional('verbose', cfgv.check_bool, False), @@ -345,6 +351,13 @@ def check(self, dct: dict[str, Any]) -> None: CONFIG_SCHEMA = cfgv.Map( 'Config', None, + # check first in case it uses some newer, incompatible feature + cfgv.Optional( + 'minimum_pre_commit_version', + cfgv.check_and(cfgv.check_string, check_min_version), + '0', + ), + cfgv.RequiredRecurse('repos', cfgv.Array(CONFIG_REPO_DICT)), cfgv.Optional( 'default_install_hook_types', @@ -358,11 +371,6 @@ def check(self, dct: dict[str, Any]) -> None: cfgv.Optional('files', check_string_regex, ''), cfgv.Optional('exclude', check_string_regex, '^$'), cfgv.Optional('fail_fast', cfgv.check_bool, False), - cfgv.Optional( - 'minimum_pre_commit_version', - cfgv.check_and(cfgv.check_string, check_min_version), - '0', - ), cfgv.WarnAdditionalKeys( ( 'repos', diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 439a09b4f..aa8418563 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -12,7 +12,6 @@ from pre_commit.clientlib import load_manifest from pre_commit.clientlib import LOCAL from pre_commit.clientlib import META -from pre_commit.clientlib import parse_version from pre_commit.hook import Hook from pre_commit.lang_base import environment_dir from pre_commit.prefix import Prefix @@ -124,15 +123,6 @@ def _hook( for dct in rest: ret.update(dct) - version = ret['minimum_pre_commit_version'] - if parse_version(version) > parse_version(C.VERSION): - logger.error( - f'The hook `{ret["id"]}` requires pre-commit version {version} ' - f'but version {C.VERSION} is installed. ' - f'Perhaps run `pip install --upgrade pre-commit`.', - ) - exit(1) - lang = ret['language'] if ret['language_version'] == C.DEFAULT: ret['language_version'] = root_config['default_language_version'][lang] diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 568b2e974..eaa8a044c 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -40,56 +40,51 @@ def test_check_type_tag_success(): @pytest.mark.parametrize( - ('config_obj', 'expected'), ( - ( - { - 'repos': [{ - 'repo': 'git@github.com:pre-commit/pre-commit-hooks', - 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', - 'hooks': [{'id': 'pyflakes', 'files': '\\.py$'}], - }], - }, - True, - ), - ( - { - 'repos': [{ - 'repo': 'git@github.com:pre-commit/pre-commit-hooks', - 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', - 'hooks': [ - { - 'id': 'pyflakes', - 'files': '\\.py$', - 'args': ['foo', 'bar', 'baz'], - }, - ], - }], - }, - True, - ), - ( - { - 'repos': [{ - 'repo': 'git@github.com:pre-commit/pre-commit-hooks', - 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', - 'hooks': [ - { - 'id': 'pyflakes', - 'files': '\\.py$', - # Exclude pattern must be a string - 'exclude': 0, - 'args': ['foo', 'bar', 'baz'], - }, - ], - }], - }, - False, - ), + 'cfg', + ( + { + 'repos': [{ + 'repo': 'git@github.com:pre-commit/pre-commit-hooks', + 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', + 'hooks': [{'id': 'pyflakes', 'files': '\\.py$'}], + }], + }, + { + 'repos': [{ + 'repo': 'git@github.com:pre-commit/pre-commit-hooks', + 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', + 'hooks': [ + { + 'id': 'pyflakes', + 'files': '\\.py$', + 'args': ['foo', 'bar', 'baz'], + }, + ], + }], + }, ), ) -def test_config_valid(config_obj, expected): - ret = is_valid_according_to_schema(config_obj, CONFIG_SCHEMA) - assert ret is expected +def test_config_valid(cfg): + assert is_valid_according_to_schema(cfg, CONFIG_SCHEMA) + + +def test_invalid_config_wrong_type(): + cfg = { + 'repos': [{ + 'repo': 'git@github.com:pre-commit/pre-commit-hooks', + 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', + 'hooks': [ + { + 'id': 'pyflakes', + 'files': '\\.py$', + # Exclude pattern must be a string + 'exclude': 0, + 'args': ['foo', 'bar', 'baz'], + }, + ], + }], + } + assert not is_valid_according_to_schema(cfg, CONFIG_SCHEMA) def test_local_hooks_with_rev_fails(): @@ -198,14 +193,13 @@ def test_warn_mutable_rev_conditional(): ), ) def test_sensible_regex_validators_dont_pass_none(validator_cls): - key = 'files' + validator = validator_cls('files', cfgv.check_string) with pytest.raises(cfgv.ValidationError) as excinfo: - validator = validator_cls(key, cfgv.check_string) - validator.check({key: None}) + validator.check({'files': None}) assert str(excinfo.value) == ( '\n' - f'==> At key: {key}' + '==> At key: files' '\n' '=====> Expected string got NoneType' ) @@ -298,46 +292,36 @@ def test_validate_optional_sensible_regex_at_top_level(caplog, regex, warning): @pytest.mark.parametrize( - ('manifest_obj', 'expected'), + 'manifest_obj', ( - ( - [{ - 'id': 'a', - 'name': 'b', - 'entry': 'c', - 'language': 'python', - 'files': r'\.py$', - }], - True, - ), - ( - [{ - 'id': 'a', - 'name': 'b', - 'entry': 'c', - 'language': 'python', - 'language_version': 'python3.4', - 'files': r'\.py$', - }], - True, - ), - ( - # A regression in 0.13.5: always_run and files are permissible - [{ - 'id': 'a', - 'name': 'b', - 'entry': 'c', - 'language': 'python', - 'files': '', - 'always_run': True, - }], - True, - ), + [{ + 'id': 'a', + 'name': 'b', + 'entry': 'c', + 'language': 'python', + 'files': r'\.py$', + }], + [{ + 'id': 'a', + 'name': 'b', + 'entry': 'c', + 'language': 'python', + 'language_version': 'python3.4', + 'files': r'\.py$', + }], + # A regression in 0.13.5: always_run and files are permissible + [{ + 'id': 'a', + 'name': 'b', + 'entry': 'c', + 'language': 'python', + 'files': '', + 'always_run': True, + }], ), ) -def test_valid_manifests(manifest_obj, expected): - ret = is_valid_according_to_schema(manifest_obj, MANIFEST_SCHEMA) - assert ret is expected +def test_valid_manifests(manifest_obj): + assert is_valid_according_to_schema(manifest_obj, MANIFEST_SCHEMA) @pytest.mark.parametrize( @@ -393,8 +377,39 @@ def test_parse_version(): def test_minimum_pre_commit_version_failing(): + cfg = {'repos': [], 'minimum_pre_commit_version': '999'} + with pytest.raises(cfgv.ValidationError) as excinfo: + cfgv.validate(cfg, CONFIG_SCHEMA) + assert str(excinfo.value) == ( + f'\n' + f'==> At Config()\n' + f'==> At key: minimum_pre_commit_version\n' + f'=====> pre-commit version 999 is required but version {C.VERSION} ' + f'is installed. Perhaps run `pip install --upgrade pre-commit`.' + ) + + +def test_minimum_pre_commit_version_failing_in_config(): + cfg = {'repos': [sample_local_config()]} + cfg['repos'][0]['hooks'][0]['minimum_pre_commit_version'] = '999' + with pytest.raises(cfgv.ValidationError) as excinfo: + cfgv.validate(cfg, CONFIG_SCHEMA) + assert str(excinfo.value) == ( + f'\n' + f'==> At Config()\n' + f'==> At key: repos\n' + f"==> At Repository(repo='local')\n" + f'==> At key: hooks\n' + f"==> At Hook(id='do_not_commit')\n" + f'==> At key: minimum_pre_commit_version\n' + f'=====> pre-commit version 999 is required but version {C.VERSION} ' + f'is installed. Perhaps run `pip install --upgrade pre-commit`.' + ) + + +def test_minimum_pre_commit_version_failing_before_other_error(): + cfg = {'repos': 5, 'minimum_pre_commit_version': '999'} with pytest.raises(cfgv.ValidationError) as excinfo: - cfg = {'repos': [], 'minimum_pre_commit_version': '999'} cfgv.validate(cfg, CONFIG_SCHEMA) assert str(excinfo.value) == ( f'\n' diff --git a/tests/repository_test.py b/tests/repository_test.py index b8dde99b4..ac065ec40 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -9,7 +9,6 @@ import cfgv import pytest -import re_assert import pre_commit.constants as C from pre_commit import lang_base @@ -27,7 +26,6 @@ from pre_commit.util import cmd_output_b from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo -from testing.fixtures import modify_manifest from testing.language_helpers import run_language from testing.util import cwd from testing.util import get_resource_path @@ -433,32 +431,6 @@ def test_hook_id_not_present(tempdir_factory, store, caplog): ) -def test_too_new_version(tempdir_factory, store, caplog): - path = make_repo(tempdir_factory, 'script_hooks_repo') - with modify_manifest(path) as manifest: - manifest[0]['minimum_pre_commit_version'] = '999.0.0' - config = make_config_from_repo(path) - with pytest.raises(SystemExit): - _get_hook(config, store, 'bash_hook') - _, msg = caplog.messages - pattern = re_assert.Matches( - r'^The hook `bash_hook` requires pre-commit version 999\.0\.0 but ' - r'version \d+\.\d+\.\d+ is installed. ' - r'Perhaps run `pip install --upgrade pre-commit`\.$', - ) - pattern.assert_matches(msg) - - -@pytest.mark.parametrize('version', ('0.1.0', C.VERSION)) -def test_versions_ok(tempdir_factory, store, version): - path = make_repo(tempdir_factory, 'script_hooks_repo') - with modify_manifest(path) as manifest: - manifest[0]['minimum_pre_commit_version'] = version - config = make_config_from_repo(path) - # Should succeed - _get_hook(config, store, 'bash_hook') - - def test_manifest_hooks(tempdir_factory, store): path = make_repo(tempdir_factory, 'script_hooks_repo') config = make_config_from_repo(path) From 08478ec176b705d17e3f7b0608d155e9dadff9bf Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 9 Dec 2023 16:04:25 -0500 Subject: [PATCH 856/967] python 3.9+: use removeprefix --- pre_commit/languages/python.py | 4 ++-- pre_commit/languages/rust.py | 2 +- tests/commands/install_uninstall_test.py | 10 ++++++---- tests/store_test.py | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index e5bac9fa8..9f4bf69a2 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -65,7 +65,7 @@ def _find_by_py_launcher( version: str, ) -> str | None: # pragma: no cover (windows only) if version.startswith('python'): - num = version[len('python'):] + num = version.removeprefix('python') cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)') env = dict(os.environ, PYTHONIOENCODING='UTF-8') try: @@ -124,7 +124,7 @@ def _sys_executable_matches(version: str) -> bool: return False try: - info = tuple(int(p) for p in version[len('python'):].split('.')) + info = tuple(int(p) for p in version.removeprefix('python').split('.')) except ValueError: return False diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 241146c57..7b04d6c25 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -134,7 +134,7 @@ def install_environment( packages_to_install: set[tuple[str, ...]] = {('--path', '.')} for cli_dep in cli_deps: - cli_dep = cli_dep[len('cli:'):] + cli_dep = cli_dep.removeprefix('cli:') package, _, crate_version = cli_dep.partition(':') if crate_version != '': packages_to_install.add((package, '--version', crate_version)) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 8b0d3ece4..9eb0e741a 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -349,8 +349,9 @@ def test_install_existing_hooks_no_overwrite(tempdir_factory, store): # We should run both the legacy and pre-commit hooks ret, output = _get_commit_output(tempdir_factory) assert ret == 0 - assert output.startswith('legacy hook\n') - NORMAL_PRE_COMMIT_RUN.assert_matches(output[len('legacy hook\n'):]) + legacy = 'legacy hook\n' + assert output.startswith(legacy) + NORMAL_PRE_COMMIT_RUN.assert_matches(output.removeprefix(legacy)) def test_legacy_overwriting_legacy_hook(tempdir_factory, store): @@ -375,8 +376,9 @@ def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory, store): # We should run both the legacy and pre-commit hooks ret, output = _get_commit_output(tempdir_factory) assert ret == 0 - assert output.startswith('legacy hook\n') - NORMAL_PRE_COMMIT_RUN.assert_matches(output[len('legacy hook\n'):]) + legacy = 'legacy hook\n' + assert output.startswith(legacy) + NORMAL_PRE_COMMIT_RUN.assert_matches(output.removeprefix(legacy)) def test_install_with_existing_non_utf8_script(tmpdir, store): diff --git a/tests/store_test.py b/tests/store_test.py index eaab94000..45ec73272 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -185,7 +185,7 @@ def test_db_repo_name(store): def test_local_resources_reflects_reality(): on_disk = { - res[len('empty_template_'):] + res.removeprefix('empty_template_') for res in os.listdir('pre_commit/resources') if res.startswith('empty_template_') } From 9c9983dba00bf67d1b2625f1f0e9112afc063849 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 9 Dec 2023 16:24:52 -0500 Subject: [PATCH 857/967] v3.6.0 --- CHANGELOG.md | 18 ++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a1b61a49..340ac476d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +3.6.0 - 2023-12-09 +================== + +### Features +- Check `minimum_pre_commit_version` first when parsing configs. + - #3092 PR by @asottile. + +### Fixes +- Fix deprecation warnings for `importlib.resources`. + - #3043 PR by @asottile. +- Fix deprecation warnings for rmtree. + - #3079 PR by @edgarrmondragon. + +### Updating +- Drop support for python<3.9. + - #3042 PR by @asottile. + - #3093 PR by @asottile. + 3.5.0 - 2023-10-13 ================== diff --git a/setup.cfg b/setup.cfg index 3110881fc..24b94e2eb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.5.0 +version = 3.6.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 9cce2834221364d4287a38469632c835142dbd62 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Dec 2023 20:20:03 +0000 Subject: [PATCH 858/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.7.1 → v1.8.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.7.1...v1.8.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4433e4e2f..2245fea10 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.7.1 + rev: v1.8.0 hooks: - id: mypy additional_dependencies: [types-all] From 9682f93e317639846cdae13b828b3d07d35e3eed Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 20:21:06 +0000 Subject: [PATCH 859/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 6.1.0 → 7.0.0](https://github.com/PyCQA/flake8/compare/6.1.0...7.0.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2245fea10..9cbda1019 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,7 +33,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 - rev: 6.1.0 + rev: 7.0.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy From 3388e2dbdf8f95d280b837db8cb9e4f7e7680bd0 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Fri, 12 Jan 2024 17:30:01 +0100 Subject: [PATCH 860/967] Pop PYTHONEXECUTABLE --- pre_commit/main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pre_commit/main.py b/pre_commit/main.py index 18c978a84..50a2e5196 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -37,6 +37,9 @@ # pyvenv os.environ.pop('__PYVENV_LAUNCHER__', None) +# https://github.com/getsentry/snuba/pull/5388 +os.environ.pop("PYTHONEXECUTABLE", None) + COMMANDS_NO_GIT = { 'clean', 'gc', 'init-templatedir', 'sample-config', 'validate-config', 'validate-manifest', From 96e0712f432ebf118a8f2963570586590d832e85 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 12 Jan 2024 16:32:43 +0000 Subject: [PATCH 861/967] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pre_commit/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 50a2e5196..559c927c9 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -38,7 +38,7 @@ os.environ.pop('__PYVENV_LAUNCHER__', None) # https://github.com/getsentry/snuba/pull/5388 -os.environ.pop("PYTHONEXECUTABLE", None) +os.environ.pop('PYTHONEXECUTABLE', None) COMMANDS_NO_GIT = { 'clean', 'gc', 'init-templatedir', 'sample-config', From 032d8e2704c9e77c04083cbcca92623a2f1e084f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 10 Feb 2024 14:01:09 -0500 Subject: [PATCH 862/967] staged_files_only can handle a crlf-only diff --- pre_commit/staged_files_only.py | 5 +++++ tests/staged_files_only_test.py | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index fd28e1c22..e1f81ba96 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -59,6 +59,11 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: # There weren't any staged files so we don't need to do anything # special yield + elif retcode == 1 and not diff_stdout.strip(): + # due to behaviour (probably a bug?) in git with crlf endings and + # autocrlf set to either `true` or `input` sometimes git will refuse + # to show a crlf-only diff to us :( + yield elif retcode == 1 and diff_stdout.strip(): patch_filename = f'patch{int(time.time())}-{os.getpid()}' patch_filename = os.path.join(patch_dir, patch_filename) diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 58dbe5ac6..cd2f63870 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -358,6 +358,21 @@ def test_crlf(in_git_dir, patch_dir, crlf_before, crlf_after, autocrlf): assert_no_diff() +@pytest.mark.parametrize('autocrlf', ('true', 'input')) +def test_crlf_diff_only(in_git_dir, patch_dir, autocrlf): + # due to a quirk (?) in git -- a diff only in crlf does not show but + # still results in an exit code of `1` + # we treat this as "no diff" -- though ideally it would discard the diff + # while committing + cmd_output('git', 'config', '--local', 'core.autocrlf', autocrlf) + + _write(b'1\r\n2\r\n3\r\n') + cmd_output('git', 'add', 'foo') + _write(b'1\n2\n3\n') + with staged_files_only(patch_dir): + pass + + def test_whitespace_errors(in_git_dir, patch_dir): cmd_output('git', 'config', '--local', 'apply.whitespace', 'error') test_crlf(in_git_dir, patch_dir, True, True, 'true') From 15bd0c7993587dc7d739ac6b1ab939eb9639bc1e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 10 Feb 2024 14:45:43 -0500 Subject: [PATCH 863/967] v3.6.1 --- CHANGELOG.md | 10 ++++++++++ setup.cfg | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 340ac476d..be2fee601 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +3.6.1 - 2024-02-10 +================== + +### Fixes +- Remove `PYTHONEXECUTABLE` from environment when running. + - #3110 PR by @untitaker. +- Handle staged-files-only with only a crlf diff. + - #3126 PR by @asottile. + - issue by @tyyrok. + 3.6.0 - 2023-12-09 ================== diff --git a/setup.cfg b/setup.cfg index 24b94e2eb..2002a6816 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.6.0 +version = 3.6.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 61d9c95cc17cb391855d17cf382feb079372644e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 18 Feb 2024 13:03:44 -0500 Subject: [PATCH 864/967] fix building golang hooks during `commit --all` --- pre_commit/languages/golang.py | 3 ++- tests/languages/golang_test.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 4c13d8f9c..66e07cf71 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -23,6 +23,7 @@ from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var +from pre_commit.git import no_git_env from pre_commit.prefix import Prefix from pre_commit.util import cmd_output from pre_commit.util import rmtree @@ -141,7 +142,7 @@ def install_environment( else: gopath = env_dir - env = dict(os.environ, GOPATH=gopath) + env = no_git_env(dict(os.environ, GOPATH=gopath)) env.pop('GOBIN', None) if version != 'system': env['GOROOT'] = os.path.join(env_dir, '.go') diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py index 19e9f62f6..02e35d710 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -7,10 +7,16 @@ import pre_commit.constants as C from pre_commit import lang_base +from pre_commit.commands.install_uninstall import install from pre_commit.envcontext import envcontext from pre_commit.languages import golang from pre_commit.store import _make_local_repo +from pre_commit.util import cmd_output +from testing.fixtures import add_config_to_repo +from testing.fixtures import make_config_from_repo from testing.language_helpers import run_language +from testing.util import cmd_output_mocked_pre_commit_home +from testing.util import git_commit ACTUAL_GET_DEFAULT_VERSION = golang.get_default_version.__wrapped__ @@ -134,3 +140,28 @@ def test_local_golang_additional_deps(tmp_path): def test_golang_hook_still_works_when_gobin_is_set(tmp_path): with envcontext((('GOBIN', str(tmp_path.joinpath('gobin'))),)): test_golang_system(tmp_path) + + +def test_during_commit_all(tmp_path, tempdir_factory, store, in_git_dir): + hook_dir = tmp_path.joinpath('hook') + hook_dir.mkdir() + _make_hello_world(hook_dir) + hook_dir.joinpath('.pre-commit-hooks.yaml').write_text( + '- id: hello-world\n' + ' name: hello world\n' + ' entry: golang-hello-world\n' + ' language: golang\n' + ' always_run: true\n', + ) + cmd_output('git', 'init', hook_dir) + cmd_output('git', 'add', '.', cwd=hook_dir) + git_commit(cwd=hook_dir) + + add_config_to_repo(in_git_dir, make_config_from_repo(hook_dir)) + + assert not install(C.CONFIG_FILE, store, hook_types=['pre-commit']) + + git_commit( + fn=cmd_output_mocked_pre_commit_home, + tempdir_factory=tempdir_factory, + ) From e5257268558a1e83731232b1ec4276a24ba870dc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 18 Feb 2024 13:19:11 -0500 Subject: [PATCH 865/967] v3.6.2 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be2fee601..6c2ee9493 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +3.6.2 - 2024-02-18 +================== + +### Fixes +- Fix building golang hooks during `git commit --all`. + - #3130 PR by @asottile. + - #2722 issue by @pestanko and @matthewhughes934. + 3.6.1 - 2024-02-10 ================== diff --git a/setup.cfg b/setup.cfg index 2002a6816..a447bbb9f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.6.1 +version = 3.6.2 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From a768c038e3ac1a6bdf04f7f2c38e7e87bf6a57ee Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 Feb 2024 00:02:29 +0000 Subject: [PATCH 866/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.15.0 → v3.15.1](https://github.com/asottile/pyupgrade/compare/v3.15.0...v3.15.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9cbda1019..c428788e9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 + rev: v3.15.1 hooks: - id: pyupgrade args: [--py39-plus] From e58009684cfc4842028e99d34837e2722af39b93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Lehmann?= Date: Wed, 7 Feb 2024 11:18:24 +0100 Subject: [PATCH 867/967] give docker a tty output when expecting color this makes the behavior more consistent with the system language and would help the executable run in a docker container to produce a colored output. --- pre_commit/languages/docker.py | 9 +++++++-- pre_commit/languages/docker_image.py | 2 +- tests/languages/docker_image_test.py | 24 ++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 26328515e..4de1d5824 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -108,10 +108,15 @@ def get_docker_user() -> tuple[str, ...]: # pragma: win32 no cover return () -def docker_cmd() -> tuple[str, ...]: # pragma: win32 no cover +def get_docker_tty(*, color: bool) -> tuple[str, ...]: # pragma: win32 no cover # noqa: E501 + return (('--tty',) if color else ()) + + +def docker_cmd(*, color: bool) -> tuple[str, ...]: # pragma: win32 no cover return ( 'docker', 'run', '--rm', + *get_docker_tty(color=color), *get_docker_user(), # https://docs.docker.com/engine/reference/commandline/run/#mount-volumes-from-container-volumes-from # The `Z` option tells Docker to label the content with a private @@ -139,7 +144,7 @@ def run_hook( entry_tag = ('--entrypoint', entry_exe, docker_tag(prefix)) return lang_base.run_xargs( - (*docker_cmd(), *entry_tag, *cmd_rest), + (*docker_cmd(color=color), *entry_tag, *cmd_rest), file_args, require_serial=require_serial, color=color, diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index a1a2c169a..60caa101d 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -23,7 +23,7 @@ def run_hook( require_serial: bool, color: bool, ) -> tuple[int, bytes]: # pragma: win32 no cover - cmd = docker_cmd() + lang_base.hook_cmd(entry, args) + cmd = docker_cmd(color=color) + lang_base.hook_cmd(entry, args) return lang_base.run_xargs( cmd, file_args, diff --git a/tests/languages/docker_image_test.py b/tests/languages/docker_image_test.py index 7993c11a8..4e3a8789a 100644 --- a/tests/languages/docker_image_test.py +++ b/tests/languages/docker_image_test.py @@ -25,3 +25,27 @@ def test_docker_image_hook_via_args(tmp_path): args=('hello hello world',), ) assert ret == (0, b'hello hello world\n') + + +@xfailif_windows # pragma: win32 no cover +def test_docker_image_color_tty(tmp_path): + ret = run_language( + tmp_path, + docker_image, + 'ubuntu:22.04', + args=('grep', '--color', 'root', '/etc/group'), + color=True, + ) + assert ret == (0, b'\x1b[01;31m\x1b[Kroot\x1b[m\x1b[K:x:0:\n') + + +@xfailif_windows # pragma: win32 no cover +def test_docker_image_no_color_no_tty(tmp_path): + ret = run_language( + tmp_path, + docker_image, + 'ubuntu:22.04', + args=('grep', '--color', 'root', '/etc/group'), + color=False, + ) + assert ret == (0, b'root:x:0:\n') From 75b3e52e57b5d6fc7bef10c131204edf196ae17a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 13 Mar 2024 00:16:12 +0000 Subject: [PATCH 868/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.8.0 → v1.9.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.8.0...v1.9.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c428788e9..229c0a8a7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.8.0 + rev: v1.9.0 hooks: - id: mypy additional_dependencies: [types-all] From 0939c11b4f0488ae3bff9b67aed67ea744189412 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Mar 2024 21:47:27 +0000 Subject: [PATCH 869/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/hhatto/autopep8: v2.0.4 → v2.1.0](https://github.com/hhatto/autopep8/compare/v2.0.4...v2.1.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 229c0a8a7..8a0ad8d7c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/hhatto/autopep8 - rev: v2.0.4 + rev: v2.1.0 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 From fc622159a6c5cd31919ed2a22fa1c11d8ca56dbf Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 24 Mar 2024 13:17:00 -0400 Subject: [PATCH 870/967] fix per-hook fail_fast to not fail on previous failures --- pre_commit/commands/run.py | 2 +- tests/commands/run_test.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 076f16d8f..2a08dff0d 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -298,7 +298,7 @@ def _run_hooks( verbose=args.verbose, use_color=args.color, ) retval |= current_retval - if retval and (config['fail_fast'] or hook.fail_fast): + if current_retval and (config['fail_fast'] or hook.fail_fast): break if retval and args.show_diff_on_failure and prior_diff: if args.all_files: diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index e36a3ca9c..50a20f377 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -1088,6 +1088,22 @@ def test_fail_fast_per_hook(cap_out, store, repo_with_failing_hook): assert printed.count(b'Failing hook') == 1 +def test_fail_fast_not_prev_failures(cap_out, store, repo_with_failing_hook): + with modify_config() as config: + config['repos'].append({ + 'repo': 'meta', + 'hooks': [ + {'id': 'identity', 'fail_fast': True}, + {'id': 'identity', 'name': 'run me!'}, + ], + }) + stage_a_file() + + ret, printed = _do_run(cap_out, store, repo_with_failing_hook, run_opts()) + # should still run the last hook since the `fail_fast` one didn't fail + assert printed.count(b'run me!') == 1 + + def test_classifier_removes_dne(): classifier = Classifier(('this_file_does_not_exist',)) assert classifier.filenames == [] From 7b4667e9e6e05e31707c404c95115b151745866c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 24 Mar 2024 13:37:19 -0400 Subject: [PATCH 871/967] v3.7.0 --- CHANGELOG.md | 17 +++++++++++++++++ setup.cfg | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c2ee9493..076e16315 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +3.7.0 - 2024-03-24 +================== + +### Features +- Use a tty for `docker` and `docker_image` hooks when `--color` is specified. + - #3122 PR by @glehmann. + +### Fixes +- Fix `fail_fast` for individual hooks stopping when previous hooks had failed. + - #3167 issue by @tp832944. + - #3168 PR by @asottile. + +### Updating +- The per-hook behaviour of `fail_fast` was fixed. If you want the pre-3.7.0 + behaviour, add `fail_fast: true` to all hooks before the last `fail_fast` + hook. + 3.6.2 - 2024-02-18 ================== diff --git a/setup.cfg b/setup.cfg index a447bbb9f..0e155601b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.6.2 +version = 3.7.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 4e121ef25c21a8caaca8304cc683e382cacd48f4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 21:31:39 +0000 Subject: [PATCH 872/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.15.1 → v3.15.2](https://github.com/asottile/pyupgrade/compare/v3.15.1...v3.15.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8a0ad8d7c..9cd3b47bb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.15.1 + rev: v3.15.2 hooks: - id: pyupgrade args: [--py39-plus] From 74d05b444de75367eaf630e099f15aa51e060dc1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 22:08:29 +0000 Subject: [PATCH 873/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.5.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.5.0...v4.6.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9cd3b47bb..93f70f871 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer From 0d4c6da36e96443f05ae2d1f6c4e63d1a5d2b652 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 29 Apr 2024 21:05:41 -0400 Subject: [PATCH 874/967] adjust _handle_readonly for typeshed updates --- pre_commit/util.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index b3682d4f7..b75c84a2d 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -205,10 +205,11 @@ def cmd_output_p( def _handle_readonly( func: Callable[[str], object], path: str, - exc: OSError, + exc: Exception, ) -> None: if ( func in (os.rmdir, os.remove, os.unlink) and + isinstance(exc, OSError) and exc.errno in {errno.EACCES, errno.EPERM} ): for p in (path, os.path.dirname(path)): @@ -222,7 +223,7 @@ def _handle_readonly( def _handle_readonly_old( func: Callable[[str], object], path: str, - excinfo: tuple[type[OSError], OSError, TracebackType], + excinfo: tuple[type[Exception], Exception, TracebackType], ) -> None: return _handle_readonly(func, path, excinfo[1]) From 5c3d006443d616f5b9a717a43a6f3bce60381ddf Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 29 Apr 2024 21:28:16 -0400 Subject: [PATCH 875/967] use a simpler gem for testing additional_dependencies tins required building bigdecimal, whereas jmespath is self-contained --- tests/languages/ruby_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 6397a4347..5d767b25d 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -91,8 +91,8 @@ def test_ruby_additional_deps(tmp_path): tmp_path, ruby, 'ruby -e', - args=('require "tins"',), - deps=('tins',), + args=('require "jmespath"',), + deps=('jmespath',), ) assert ret == (0, b'') From 0142f453224801138448584a8517927194865330 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 22:03:55 +0000 Subject: [PATCH 876/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.9.0 → v1.10.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.9.0...v1.10.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 93f70f871..6caee40d7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.9.0 + rev: v1.10.0 hooks: - id: mypy additional_dependencies: [types-all] From 296f59266ec656fe46bf0d1b2bce6aac89476476 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 10 May 2024 17:06:29 -0400 Subject: [PATCH 877/967] determine rust default language version independent of rust-toolchain.toml --- pre_commit/languages/rust.py | 2 +- tests/languages/rust_test.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 7b04d6c25..5f9db8fb7 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -34,7 +34,7 @@ def get_default_version() -> str: # Just detecting the executable does not suffice, because if rustup is # installed but no toolchain is available, then `cargo` exists but # cannot be used without installing a toolchain first. - if cmd_output_b('cargo', '--version', check=False)[0] == 0: + if cmd_output_b('cargo', '--version', check=False, cwd='/')[0] == 0: return 'system' else: return C.DEFAULT diff --git a/tests/languages/rust_test.py b/tests/languages/rust_test.py index 5c17f5b69..52e356134 100644 --- a/tests/languages/rust_test.py +++ b/tests/languages/rust_test.py @@ -9,6 +9,7 @@ from pre_commit.languages import rust from pre_commit.store import _make_local_repo from testing.language_helpers import run_language +from testing.util import cwd ACTUAL_GET_DEFAULT_VERSION = rust.get_default_version.__wrapped__ @@ -29,6 +30,14 @@ def test_uses_default_when_rust_is_not_available(cmd_output_b_mck): assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT +def test_selects_system_even_if_rust_toolchain_toml(tmp_path): + toolchain_toml = '[toolchain]\nchannel = "wtf"\n' + tmp_path.joinpath('rust-toolchain.toml').write_text(toolchain_toml) + + with cwd(tmp_path): + assert ACTUAL_GET_DEFAULT_VERSION() == 'system' + + def _make_hello_world(tmp_path): src_dir = tmp_path.joinpath('src') src_dir.mkdir() From 9ee076835365c0b3aa700de8f574def623826385 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 10 May 2024 21:24:51 -0400 Subject: [PATCH 878/967] v3.7.1 --- CHANGELOG.md | 9 +++++++++ setup.cfg | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 076e16315..81d5b33e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +3.7.1 - 2024-05-10 +================== + +### Fixes +- Fix `language: rust` default language version check when `rust-toolchain.toml` + is present. + - issue by @gaborbernat. + - #3201 PR by @asottile. + 3.7.0 - 2024-03-24 ================== diff --git a/setup.cfg b/setup.cfg index 0e155601b..83c09acde 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.7.0 +version = 3.7.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 5526bb21377dc3e4a59451a55d0d729644eac462 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 21:34:15 +0000 Subject: [PATCH 879/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/hhatto/autopep8: v2.1.0 → v2.1.1](https://github.com/hhatto/autopep8/compare/v2.1.0...v2.1.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6caee40d7..eebeea992 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/hhatto/autopep8 - rev: v2.1.0 + rev: v2.1.1 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 From 1f128556e4ac2fae84133b9a4f085a8044a44382 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Jun 2024 21:47:18 +0000 Subject: [PATCH 880/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder-python-imports: v3.12.0 → v3.13.0](https://github.com/asottile/reorder-python-imports/compare/v3.12.0...v3.13.0) - [github.com/hhatto/autopep8: v2.1.1 → v2.2.0](https://github.com/hhatto/autopep8/compare/v2.1.1...v2.2.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eebeea992..0467fa394 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports - rev: v3.12.0 + rev: v3.13.0 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) @@ -29,7 +29,7 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/hhatto/autopep8 - rev: v2.1.1 + rev: v2.2.0 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 From 9dd247898c86405b68705595d8a3c8911be39d57 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Jun 2024 21:56:51 +0000 Subject: [PATCH 881/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.15.2 → v3.16.0](https://github.com/asottile/pyupgrade/compare/v3.15.2...v3.16.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0467fa394..6282056f9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.15.2 + rev: v3.16.0 hooks: - id: pyupgrade args: [--py39-plus] From 49a9664cd0e393fb3bc5e1023bee801cc3e6fc6a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 21:57:20 +0000 Subject: [PATCH 882/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/hhatto/autopep8: v2.2.0 → v2.3.0](https://github.com/hhatto/autopep8/compare/v2.2.0...v2.3.0) - [github.com/PyCQA/flake8: 7.0.0 → 7.1.0](https://github.com/PyCQA/flake8/compare/7.0.0...7.1.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6282056f9..b11a1dce2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,11 +29,11 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/hhatto/autopep8 - rev: v2.2.0 + rev: v2.3.0 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 - rev: 7.0.0 + rev: 7.1.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy From 69b5dce12ab0674cd7a622ca8b55f1afa720211b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 24 Jun 2024 21:49:02 +0000 Subject: [PATCH 883/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/hhatto/autopep8: v2.3.0 → v2.3.1](https://github.com/hhatto/autopep8/compare/v2.3.0...v2.3.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b11a1dce2..1f734f8cf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/hhatto/autopep8 - rev: v2.3.0 + rev: v2.3.1 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 From f632459bc67834a200aac26f1129fc16f82fb625 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Jul 2024 23:34:14 +0000 Subject: [PATCH 884/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.10.0 → v1.10.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.10.0...v1.10.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1f734f8cf..f987dfe89 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.10.0 + rev: v1.10.1 hooks: - id: mypy additional_dependencies: [types-all] From 88317ddb34ac8c60b4be7e22198fb550dcae995e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Jul 2024 22:04:19 +0000 Subject: [PATCH 885/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.10.1 → v1.11.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.10.1...v1.11.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f987dfe89..a628f4f47 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.10.1 + rev: v1.11.0 hooks: - id: mypy additional_dependencies: [types-all] From a68a19d217d0d1067828622fde9044d9502693b3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 28 Jul 2024 14:50:24 -0400 Subject: [PATCH 886/967] fixes for mypy 1.11 --- .pre-commit-config.yaml | 2 +- pre_commit/util.py | 4 ++-- tests/conftest.py | 25 +++++++------------------ 3 files changed, 10 insertions(+), 21 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a628f4f47..1a9a8fef9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,5 +40,5 @@ repos: rev: v1.11.0 hooks: - id: mypy - additional_dependencies: [types-all] + additional_dependencies: [types-pyyaml] exclude: ^testing/resources/ diff --git a/pre_commit/util.py b/pre_commit/util.py index b75c84a2d..12aa3c0e1 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -205,7 +205,7 @@ def cmd_output_p( def _handle_readonly( func: Callable[[str], object], path: str, - exc: Exception, + exc: BaseException, ) -> None: if ( func in (os.rmdir, os.remove, os.unlink) and @@ -223,7 +223,7 @@ def _handle_readonly( def _handle_readonly_old( func: Callable[[str], object], path: str, - excinfo: tuple[type[Exception], Exception, TracebackType], + excinfo: tuple[type[BaseException], BaseException, TracebackType], ) -> None: return _handle_readonly(func, path, excinfo[1]) diff --git a/tests/conftest.py b/tests/conftest.py index 30761715b..bd4af9a52 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -209,36 +209,25 @@ def log_info_mock(): yield mck -class FakeStream: - def __init__(self): - self.data = io.BytesIO() - - def write(self, s): - self.data.write(s) - - def flush(self): - pass - - class Fixture: - def __init__(self, stream): + def __init__(self, stream: io.BytesIO) -> None: self._stream = stream - def get_bytes(self): + def get_bytes(self) -> bytes: """Get the output as-if no encoding occurred""" - data = self._stream.data.getvalue() - self._stream.data.seek(0) - self._stream.data.truncate() + data = self._stream.getvalue() + self._stream.seek(0) + self._stream.truncate() return data.replace(b'\r\n', b'\n') - def get(self): + def get(self) -> str: """Get the output assuming it was written as UTF-8 bytes""" return self.get_bytes().decode() @pytest.fixture def cap_out(): - stream = FakeStream() + stream = io.BytesIO() write = functools.partial(output.write, stream=stream) write_line_b = functools.partial(output.write_line_b, stream=stream) with mock.patch.multiple(output, write=write, write_line_b=write_line_b): From da0c1d0cfa19f6dc0d6ed97820c7cc93fe7e7c58 Mon Sep 17 00:00:00 2001 From: Lorenz Walthert Date: Mon, 22 Jul 2024 20:52:43 +0200 Subject: [PATCH 887/967] implement health check for language:r --- pre_commit/languages/r.py | 77 +++++++++++++++++++++++++---- tests/languages/r_test.py | 100 +++++++++++++++++++++++++++++++++----- 2 files changed, 155 insertions(+), 22 deletions(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 93b62bd53..5d18bf1cb 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -14,13 +14,74 @@ from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.prefix import Prefix +from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b from pre_commit.util import win_exe ENVIRONMENT_DIR = 'renv' RSCRIPT_OPTS = ('--no-save', '--no-restore', '--no-site-file', '--no-environ') get_default_version = lang_base.basic_get_default_version -health_check = lang_base.basic_health_check + + +def _execute_vanilla_r_code_as_script( + code: str, *, + prefix: Prefix, version: str, args: Sequence[str] = (), cwd: str, +) -> str: + with in_env(prefix, version), _r_code_in_tempfile(code) as f: + _, out, _ = cmd_output( + _rscript_exec(), *RSCRIPT_OPTS, f, *args, cwd=cwd, + ) + return out.rstrip('\n') + + +def _read_installed_version(envdir: str, prefix: Prefix, version: str) -> str: + return _execute_vanilla_r_code_as_script( + 'cat(renv::settings$r.version())', + prefix=prefix, version=version, + cwd=envdir, + ) + + +def _read_executable_version(envdir: str, prefix: Prefix, version: str) -> str: + return _execute_vanilla_r_code_as_script( + 'cat(as.character(getRversion()))', + prefix=prefix, version=version, + cwd=envdir, + ) + + +def _write_current_r_version( + envdir: str, prefix: Prefix, version: str, +) -> None: + _execute_vanilla_r_code_as_script( + 'renv::settings$r.version(as.character(getRversion()))', + prefix=prefix, version=version, + cwd=envdir, + ) + + +def health_check(prefix: Prefix, version: str) -> str | None: + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) + + r_version_installation = _read_installed_version( + envdir=envdir, prefix=prefix, version=version, + ) + r_version_current_executable = _read_executable_version( + envdir=envdir, prefix=prefix, version=version, + ) + if r_version_installation in {'NULL', ''}: + return ( + f'Hooks were installed with an unknown R version. R version for ' + f'hook repo now set to {r_version_current_executable}' + ) + elif r_version_installation != r_version_current_executable: + return ( + f'Hooks were installed for R version {r_version_installation}, ' + f'but current R executable has version ' + f'{r_version_current_executable}' + ) + + return None @contextlib.contextmanager @@ -147,16 +208,14 @@ def install_environment( with _r_code_in_tempfile(r_code_inst_environment) as f: cmd_output_b(_rscript_exec(), '--vanilla', f, cwd=env_dir) + _write_current_r_version(envdir=env_dir, prefix=prefix, version=version) if additional_dependencies: r_code_inst_add = 'renv::install(commandArgs(trailingOnly = TRUE))' - with in_env(prefix, version): - with _r_code_in_tempfile(r_code_inst_add) as f: - cmd_output_b( - _rscript_exec(), *RSCRIPT_OPTS, - f, - *additional_dependencies, - cwd=env_dir, - ) + _execute_vanilla_r_code_as_script( + code=r_code_inst_add, prefix=prefix, version=version, + args=additional_dependencies, + cwd=env_dir, + ) def _inline_r_setup(code: str) -> str: diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py index 02c559cb4..10919e4a7 100644 --- a/tests/languages/r_test.py +++ b/tests/languages/r_test.py @@ -1,14 +1,17 @@ from __future__ import annotations import os.path -import shutil +from unittest import mock import pytest +import pre_commit.constants as C from pre_commit import envcontext +from pre_commit import lang_base from pre_commit.languages import r from pre_commit.prefix import Prefix from pre_commit.store import _make_local_repo +from pre_commit.util import resource_text from pre_commit.util import win_exe from testing.language_helpers import run_language @@ -127,7 +130,8 @@ def test_path_rscript_exec_no_r_home_set(): assert r._rscript_exec() == 'Rscript' -def test_r_hook(tmp_path): +@pytest.fixture +def renv_lock_file(tmp_path): renv_lock = '''\ { "R": { @@ -157,6 +161,12 @@ def test_r_hook(tmp_path): } } ''' + tmp_path.joinpath('renv.lock').write_text(renv_lock) + yield + + +@pytest.fixture +def description_file(tmp_path): description = '''\ Package: gli.clu Title: What the Package Does (One Line, Title Case) @@ -178,27 +188,39 @@ def test_r_hook(tmp_path): Imports: rprojroot ''' - hello_world_r = '''\ + tmp_path.joinpath('DESCRIPTION').write_text(description) + yield + + +@pytest.fixture +def hello_world_file(tmp_path): + hello_world = '''\ stopifnot( packageVersion('rprojroot') == '1.0', packageVersion('gli.clu') == '0.0.0.9000' ) cat("Hello, World, from R!\n") ''' + tmp_path.joinpath('hello-world.R').write_text(hello_world) + yield - tmp_path.joinpath('renv.lock').write_text(renv_lock) - tmp_path.joinpath('DESCRIPTION').write_text(description) - tmp_path.joinpath('hello-world.R').write_text(hello_world_r) + +@pytest.fixture +def renv_folder(tmp_path): renv_dir = tmp_path.joinpath('renv') renv_dir.mkdir() - shutil.copy( - os.path.join( - os.path.dirname(__file__), - '../../pre_commit/resources/empty_template_activate.R', - ), - renv_dir.joinpath('activate.R'), - ) + activate_r = resource_text('empty_template_activate.R') + renv_dir.joinpath('activate.R').write_text(activate_r) + yield + +def test_r_hook( + tmp_path, + renv_lock_file, + description_file, + hello_world_file, + renv_folder, +): expected = (0, b'Hello, World, from R!\n') assert run_language(tmp_path, r, 'Rscript hello-world.R') == expected @@ -221,3 +243,55 @@ def test_r_inline(tmp_path): args=('hi', 'hello'), ) assert ret == (0, b'hi, hello, from R!\n') + + +@pytest.fixture +def prefix(tmpdir): + yield Prefix(str(tmpdir)) + + +@pytest.fixture +def installed_environment( + renv_lock_file, + hello_world_file, + renv_folder, + prefix, +): + env_dir = lang_base.environment_dir( + prefix, r.ENVIRONMENT_DIR, r.get_default_version(), + ) + r.install_environment(prefix, C.DEFAULT, ()) + yield prefix, env_dir + + +def test_health_check_healthy(installed_environment): + # should be healthy right after creation + prefix, _ = installed_environment + assert r.health_check(prefix, C.DEFAULT) is None + + +def test_health_check_after_downgrade(installed_environment): + prefix, _ = installed_environment + + # pretend the saved installed version is old + with mock.patch.object(r, '_read_installed_version', return_value='1.0.0'): + output = r.health_check(prefix, C.DEFAULT) + + assert output is not None + assert output.startswith('Hooks were installed for R version') + + +@pytest.mark.parametrize('version', ('NULL', 'NA', "''")) +def test_health_check_without_version(prefix, installed_environment, version): + prefix, env_dir = installed_environment + + # simulate old pre-commit install by unsetting the installed version + r._execute_vanilla_r_code_as_script( + f'renv::settings$r.version({version})', + prefix=prefix, version=C.DEFAULT, cwd=env_dir, + ) + + # no R version specified fails as unhealty + msg = 'Hooks were installed with an unknown R version' + check_output = r.health_check(prefix, C.DEFAULT) + assert check_output is not None and check_output.startswith(msg) From d46423ffe14a37a06a0bcb6fe1b8294a27b6c289 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 28 Jul 2024 15:58:29 -0400 Subject: [PATCH 888/967] v3.8.0 --- CHANGELOG.md | 9 +++++++++ setup.cfg | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81d5b33e8..49094bbb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +3.8.0 - 2024-07-28 +================== + +### Features +- Implement health checks for `language: r` so environments are recreated if + the system version of R changes. + - #3206 issue by @lorenzwalthert. + - #3265 PR by @lorenzwalthert. + 3.7.1 - 2024-05-10 ================== diff --git a/setup.cfg b/setup.cfg index 83c09acde..52b7681ef 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.7.1 +version = 3.8.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 9d4ab670d18f3c32ee204dbb50af74884d832ce4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 21:59:01 +0000 Subject: [PATCH 889/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.16.0 → v3.17.0](https://github.com/asottile/pyupgrade/compare/v3.16.0...v3.17.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1a9a8fef9..16cec4cde 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.16.0 + rev: v3.17.0 hooks: - id: pyupgrade args: [--py39-plus] From 917e2102be90a6384cf514ddc0edefbc563b49fc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 21:59:19 +0000 Subject: [PATCH 890/967] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pre_commit/commands/run.py | 6 +++--- pre_commit/envcontext.py | 2 +- pre_commit/error_handler.py | 2 +- pre_commit/file_lock.py | 6 +++--- pre_commit/lang_base.py | 2 +- pre_commit/languages/conda.py | 2 +- pre_commit/languages/coursier.py | 2 +- pre_commit/languages/dart.py | 2 +- pre_commit/languages/dotnet.py | 4 ++-- pre_commit/languages/golang.py | 2 +- pre_commit/languages/haskell.py | 2 +- pre_commit/languages/lua.py | 2 +- pre_commit/languages/node.py | 2 +- pre_commit/languages/perl.py | 2 +- pre_commit/languages/python.py | 2 +- pre_commit/languages/r.py | 4 ++-- pre_commit/languages/ruby.py | 2 +- pre_commit/languages/rust.py | 2 +- pre_commit/languages/swift.py | 2 +- pre_commit/logging_handler.py | 2 +- pre_commit/staged_files_only.py | 6 +++--- pre_commit/store.py | 4 ++-- pre_commit/util.py | 2 +- pre_commit/xargs.py | 1 - 24 files changed, 32 insertions(+), 33 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 2a08dff0d..793adbdb2 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -61,7 +61,7 @@ def filter_by_include_exclude( names: Iterable[str], include: str, exclude: str, -) -> Generator[str, None, None]: +) -> Generator[str]: include_re, exclude_re = re.compile(include), re.compile(exclude) return ( filename for filename in names @@ -84,7 +84,7 @@ def by_types( types: Iterable[str], types_or: Iterable[str], exclude_types: Iterable[str], - ) -> Generator[str, None, None]: + ) -> Generator[str]: types = frozenset(types) types_or = frozenset(types_or) exclude_types = frozenset(exclude_types) @@ -97,7 +97,7 @@ def by_types( ): yield filename - def filenames_for_hook(self, hook: Hook) -> Generator[str, None, None]: + def filenames_for_hook(self, hook: Hook) -> Generator[str]: return self.by_types( filter_by_include_exclude( self.filenames, diff --git a/pre_commit/envcontext.py b/pre_commit/envcontext.py index 1f816cea9..d4d241184 100644 --- a/pre_commit/envcontext.py +++ b/pre_commit/envcontext.py @@ -33,7 +33,7 @@ def format_env(parts: SubstitutionT, env: MutableMapping[str, str]) -> str: def envcontext( patch: PatchesT, _env: MutableMapping[str, str] | None = None, -) -> Generator[None, None, None]: +) -> Generator[None]: """In this context, `os.environ` is modified according to `patch`. `patch` is an iterable of 2-tuples (key, value): diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 73e608b71..4f0e05733 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -68,7 +68,7 @@ def _log_and_exit( @contextlib.contextmanager -def error_handler() -> Generator[None, None, None]: +def error_handler() -> Generator[None]: try: yield except (Exception, KeyboardInterrupt) as e: diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index d3dafb4da..c840ad8b5 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -20,7 +20,7 @@ def _locked( fileno: int, blocked_cb: Callable[[], None], - ) -> Generator[None, None, None]: + ) -> Generator[None]: try: msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region) except OSError: @@ -53,7 +53,7 @@ def _locked( def _locked( fileno: int, blocked_cb: Callable[[], None], - ) -> Generator[None, None, None]: + ) -> Generator[None]: try: fcntl.flock(fileno, fcntl.LOCK_EX | fcntl.LOCK_NB) except OSError: # pragma: no cover (tests are single-threaded) @@ -69,7 +69,7 @@ def _locked( def lock( path: str, blocked_cb: Callable[[], None], -) -> Generator[None, None, None]: +) -> Generator[None]: with open(path, 'a+') as f: with _locked(f.fileno(), blocked_cb): yield diff --git a/pre_commit/lang_base.py b/pre_commit/lang_base.py index 5303948b5..95be7b9b3 100644 --- a/pre_commit/lang_base.py +++ b/pre_commit/lang_base.py @@ -127,7 +127,7 @@ def no_install( @contextlib.contextmanager -def no_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def no_env(prefix: Prefix, version: str) -> Generator[None]: yield diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index 80b3e1507..d397ebeb7 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -41,7 +41,7 @@ def get_env_patch(env: str) -> PatchesT: @contextlib.contextmanager -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index 6558bf6b8..08f9a958f 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -70,7 +70,7 @@ def get_env_patch(target_dir: str) -> PatchesT: @contextlib.contextmanager -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py index 129ac5918..52a229eef 100644 --- a/pre_commit/languages/dart.py +++ b/pre_commit/languages/dart.py @@ -29,7 +29,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index e1202c4f2..ffc65d1e8 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -30,14 +30,14 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @contextlib.contextmanager -def _nuget_config_no_sources() -> Generator[str, None, None]: +def _nuget_config_no_sources() -> Generator[str]: with tempfile.TemporaryDirectory() as tmpdir: nuget_config = os.path.join(tmpdir, 'nuget.config') with open(nuget_config, 'w') as f: diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 66e07cf71..609087962 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -121,7 +121,7 @@ def _install_go(version: str, dest: str) -> None: @contextlib.contextmanager -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir, version)): yield diff --git a/pre_commit/languages/haskell.py b/pre_commit/languages/haskell.py index c6945c822..28bca08cc 100644 --- a/pre_commit/languages/haskell.py +++ b/pre_commit/languages/haskell.py @@ -24,7 +24,7 @@ def get_env_patch(target_dir: str) -> PatchesT: @contextlib.contextmanager -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield diff --git a/pre_commit/languages/lua.py b/pre_commit/languages/lua.py index a475ec99c..15ac1a2ec 100644 --- a/pre_commit/languages/lua.py +++ b/pre_commit/languages/lua.py @@ -44,7 +44,7 @@ def get_env_patch(d: str) -> PatchesT: # pragma: win32 no cover @contextlib.contextmanager # pragma: win32 no cover -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index d49c0e326..af7dc6f87 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -59,7 +59,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py index 61b1d114b..a07d442ac 100644 --- a/pre_commit/languages/perl.py +++ b/pre_commit/languages/perl.py @@ -33,7 +33,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 9f4bf69a2..0c4bb62db 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -152,7 +152,7 @@ def norm_version(version: str) -> str | None: @contextlib.contextmanager -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 5d18bf1cb..c75a30893 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -85,7 +85,7 @@ def health_check(prefix: Prefix, version: str) -> str | None: @contextlib.contextmanager -def _r_code_in_tempfile(code: str) -> Generator[str, None, None]: +def _r_code_in_tempfile(code: str) -> Generator[str]: """ To avoid quoting and escaping issues, avoid `Rscript [options] -e {expr}` but use `Rscript [options] path/to/file_with_expr.R` @@ -105,7 +105,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 0438ae095..f32fea3fa 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -73,7 +73,7 @@ def get_env_patch( @contextlib.contextmanager -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir, version)): yield diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 5f9db8fb7..fd77a9d29 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -61,7 +61,7 @@ def get_env_patch(target_dir: str, version: str) -> PatchesT: @contextlib.contextmanager -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir, version)): yield diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index f7bfe84c5..08a9c39a8 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -27,7 +27,7 @@ def get_env_patch(venv: str) -> PatchesT: # pragma: win32 no cover @contextlib.contextmanager # pragma: win32 no cover -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield diff --git a/pre_commit/logging_handler.py b/pre_commit/logging_handler.py index cd33953d7..74772beee 100644 --- a/pre_commit/logging_handler.py +++ b/pre_commit/logging_handler.py @@ -32,7 +32,7 @@ def emit(self, record: logging.LogRecord) -> None: @contextlib.contextmanager -def logging_handler(use_color: bool) -> Generator[None, None, None]: +def logging_handler(use_color: bool) -> Generator[None]: handler = LoggingHandler(use_color) logger.addHandler(handler) logger.setLevel(logging.INFO) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index e1f81ba96..99ea0979b 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -33,7 +33,7 @@ def _git_apply(patch: str) -> None: @contextlib.contextmanager -def _intent_to_add_cleared() -> Generator[None, None, None]: +def _intent_to_add_cleared() -> Generator[None]: intent_to_add = git.intent_to_add_files() if intent_to_add: logger.warning('Unstaged intent-to-add files detected.') @@ -48,7 +48,7 @@ def _intent_to_add_cleared() -> Generator[None, None, None]: @contextlib.contextmanager -def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: +def _unstaged_changes_cleared(patch_dir: str) -> Generator[None]: tree = cmd_output('git', 'write-tree')[1].strip() diff_cmd = ( 'git', 'diff-index', '--ignore-submodules', '--binary', @@ -105,7 +105,7 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: @contextlib.contextmanager -def staged_files_only(patch_dir: str) -> Generator[None, None, None]: +def staged_files_only(patch_dir: str) -> Generator[None]: """Clear any unstaged changes from the git working directory inside this context. """ diff --git a/pre_commit/store.py b/pre_commit/store.py index 84bc09a4c..36cc49456 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -101,7 +101,7 @@ def __init__(self, directory: str | None = None) -> None: os.replace(tmpfile, self.db_path) @contextlib.contextmanager - def exclusive_lock(self) -> Generator[None, None, None]: + def exclusive_lock(self) -> Generator[None]: def blocked_cb() -> None: # pragma: no cover (tests are in-process) logger.info('Locking pre-commit directory') @@ -112,7 +112,7 @@ def blocked_cb() -> None: # pragma: no cover (tests are in-process) def connect( self, db_path: str | None = None, - ) -> Generator[sqlite3.Connection, None, None]: + ) -> Generator[sqlite3.Connection]: db_path = db_path or self.db_path # sqlite doesn't close its fd with its contextmanager >.< # contextlib.closing fixes this. diff --git a/pre_commit/util.py b/pre_commit/util.py index 12aa3c0e1..e199d0807 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -25,7 +25,7 @@ def force_bytes(exc: Any) -> bytes: @contextlib.contextmanager -def clean_path_on_failure(path: str) -> Generator[None, None, None]: +def clean_path_on_failure(path: str) -> Generator[None]: """Cleans up the directory on an exceptional failure.""" try: yield diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 22580f595..a1345b583 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -120,7 +120,6 @@ def partition( @contextlib.contextmanager def _thread_mapper(maxsize: int) -> Generator[ Callable[[Callable[[TArg], TRet], Iterable[TArg]], Iterable[TRet]], - None, None, ]: if maxsize == 1: yield map From d5c21926ab78fd3d89f4891b29bd426f6ee80c9c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 22:39:33 +0000 Subject: [PATCH 891/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 7.1.0 → 7.1.1](https://github.com/PyCQA/flake8/compare/7.1.0...7.1.1) - [github.com/pre-commit/mirrors-mypy: v1.11.0 → v1.11.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.11.0...v1.11.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 16cec4cde..a6c853caa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,11 +33,11 @@ repos: hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 - rev: 7.1.0 + rev: 7.1.1 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.11.0 + rev: v1.11.1 hooks: - id: mypy additional_dependencies: [types-pyyaml] From c2c68d985ceac41afe63635c15789207c441614e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 22:18:35 +0000 Subject: [PATCH 892/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.11.1 → v1.11.2](https://github.com/pre-commit/mirrors-mypy/compare/v1.11.1...v1.11.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a6c853caa..87b8551d4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.11.1 + rev: v1.11.2 hooks: - id: mypy additional_dependencies: [types-pyyaml] From 364e6d77f051b40d22ac9071ef64bc12f3e6a1fe Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 16 Sep 2024 20:05:29 -0400 Subject: [PATCH 893/967] change migrate-config to use yaml parse tree instead --- pre_commit/commands/migrate_config.py | 58 ++++++++++++++++++++++----- pre_commit/yaml.py | 1 + pre_commit/yaml_rewrite.py | 52 ++++++++++++++++++++++++ tests/commands/migrate_config_test.py | 46 +++++++++++++++++++++ tests/yaml_rewrite_test.py | 47 ++++++++++++++++++++++ 5 files changed, 194 insertions(+), 10 deletions(-) create mode 100644 pre_commit/yaml_rewrite.py create mode 100644 tests/yaml_rewrite_test.py diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index 842fb3a7b..cdce83f54 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -1,13 +1,20 @@ from __future__ import annotations -import re +import functools import textwrap +from typing import Callable import cfgv import yaml +from yaml.nodes import ScalarNode from pre_commit.clientlib import InvalidConfigError +from pre_commit.yaml import yaml_compose from pre_commit.yaml import yaml_load +from pre_commit.yaml_rewrite import MappingKey +from pre_commit.yaml_rewrite import MappingValue +from pre_commit.yaml_rewrite import match +from pre_commit.yaml_rewrite import SequenceItem def _is_header_line(line: str) -> bool: @@ -38,16 +45,48 @@ def _migrate_map(contents: str) -> str: return contents -def _migrate_sha_to_rev(contents: str) -> str: - return re.sub(r'(\n\s+)sha:', r'\1rev:', contents) +def _preserve_style(n: ScalarNode, *, s: str) -> str: + return f'{n.style}{s}{n.style}' -def _migrate_python_venv(contents: str) -> str: - return re.sub( - r'(\n\s+)language: python_venv\b', - r'\1language: python', - contents, +def _migrate_composed(contents: str) -> str: + tree = yaml_compose(contents) + rewrites: list[tuple[ScalarNode, Callable[[ScalarNode], str]]] = [] + + # sha -> rev + sha_to_rev_replace = functools.partial(_preserve_style, s='rev') + sha_to_rev_matcher = ( + MappingValue('repos'), + SequenceItem(), + MappingKey('sha'), + ) + for node in match(tree, sha_to_rev_matcher): + rewrites.append((node, sha_to_rev_replace)) + + # python_venv -> python + language_matcher = ( + MappingValue('repos'), + SequenceItem(), + MappingValue('hooks'), + SequenceItem(), + MappingValue('language'), ) + python_venv_replace = functools.partial(_preserve_style, s='python') + for node in match(tree, language_matcher): + if node.value == 'python_venv': + rewrites.append((node, python_venv_replace)) + + rewrites.sort(reverse=True, key=lambda nf: nf[0].start_mark.index) + + src_parts = [] + end: int | None = None + for node, func in rewrites: + src_parts.append(contents[node.end_mark.index:end]) + src_parts.append(func(node)) + end = node.start_mark.index + src_parts.append(contents[:end]) + src_parts.reverse() + return ''.join(src_parts) def migrate_config(config_file: str, quiet: bool = False) -> int: @@ -62,8 +101,7 @@ def migrate_config(config_file: str, quiet: bool = False) -> int: raise cfgv.ValidationError(str(e)) contents = _migrate_map(contents) - contents = _migrate_sha_to_rev(contents) - contents = _migrate_python_venv(contents) + contents = _migrate_composed(contents) if contents != orig_contents: with open(config_file, 'w') as f: diff --git a/pre_commit/yaml.py b/pre_commit/yaml.py index bdf4ec47d..a5bbbc999 100644 --- a/pre_commit/yaml.py +++ b/pre_commit/yaml.py @@ -6,6 +6,7 @@ import yaml Loader = getattr(yaml, 'CSafeLoader', yaml.SafeLoader) +yaml_compose = functools.partial(yaml.compose, Loader=Loader) yaml_load = functools.partial(yaml.load, Loader=Loader) Dumper = getattr(yaml, 'CSafeDumper', yaml.SafeDumper) diff --git a/pre_commit/yaml_rewrite.py b/pre_commit/yaml_rewrite.py new file mode 100644 index 000000000..8d0e8fdb2 --- /dev/null +++ b/pre_commit/yaml_rewrite.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from collections.abc import Generator +from collections.abc import Iterable +from typing import NamedTuple +from typing import Protocol + +from yaml.nodes import MappingNode +from yaml.nodes import Node +from yaml.nodes import ScalarNode +from yaml.nodes import SequenceNode + + +class _Matcher(Protocol): + def match(self, n: Node) -> Generator[Node]: ... + + +class MappingKey(NamedTuple): + k: str + + def match(self, n: Node) -> Generator[Node]: + if isinstance(n, MappingNode): + for k, _ in n.value: + if k.value == self.k: + yield k + + +class MappingValue(NamedTuple): + k: str + + def match(self, n: Node) -> Generator[Node]: + if isinstance(n, MappingNode): + for k, v in n.value: + if k.value == self.k: + yield v + + +class SequenceItem(NamedTuple): + def match(self, n: Node) -> Generator[Node]: + if isinstance(n, SequenceNode): + yield from n.value + + +def _match(gen: Iterable[Node], m: _Matcher) -> Iterable[Node]: + return (n for src in gen for n in m.match(src)) + + +def match(n: Node, matcher: tuple[_Matcher, ...]) -> Generator[ScalarNode]: + gen: Iterable[Node] = (n,) + for m in matcher: + gen = _match(gen, m) + return (n for n in gen if isinstance(n, ScalarNode)) diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index ba1846360..c563866d9 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -134,6 +134,27 @@ def test_migrate_config_sha_to_rev(tmpdir): ) +def test_migrate_config_sha_to_rev_json(tmp_path): + contents = """\ +{"repos": [{ + "repo": "https://github.com/pre-commit/pre-commit-hooks", + "sha": "v1.2.0", + "hooks": [] +}]} +""" + expected = """\ +{"repos": [{ + "repo": "https://github.com/pre-commit/pre-commit-hooks", + "rev": "v1.2.0", + "hooks": [] +}]} +""" + cfg = tmp_path.joinpath('cfg.yaml') + cfg.write_text(contents) + assert not migrate_config(str(cfg)) + assert cfg.read_text() == expected + + def test_migrate_config_language_python_venv(tmp_path): src = '''\ repos: @@ -167,6 +188,31 @@ def test_migrate_config_language_python_venv(tmp_path): assert cfg.read_text() == expected +def test_migrate_config_quoted_python_venv(tmp_path): + src = '''\ +repos: +- repo: local + hooks: + - id: example + name: example + entry: example + language: "python_venv" +''' + expected = '''\ +repos: +- repo: local + hooks: + - id: example + name: example + entry: example + language: "python" +''' + cfg = tmp_path.joinpath('cfg.yaml') + cfg.write_text(src) + assert migrate_config(str(cfg)) == 0 + assert cfg.read_text() == expected + + def test_migrate_config_invalid_yaml(tmpdir): contents = '[' cfg = tmpdir.join(C.CONFIG_FILE) diff --git a/tests/yaml_rewrite_test.py b/tests/yaml_rewrite_test.py new file mode 100644 index 000000000..d0f6841cf --- /dev/null +++ b/tests/yaml_rewrite_test.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +import pytest + +from pre_commit.yaml import yaml_compose +from pre_commit.yaml_rewrite import MappingKey +from pre_commit.yaml_rewrite import MappingValue +from pre_commit.yaml_rewrite import match +from pre_commit.yaml_rewrite import SequenceItem + + +def test_match_produces_scalar_values_only(): + src = '''\ +- name: foo +- name: [not, foo] # not a scalar: should be skipped! +- name: bar +''' + matcher = (SequenceItem(), MappingValue('name')) + ret = [n.value for n in match(yaml_compose(src), matcher)] + assert ret == ['foo', 'bar'] + + +@pytest.mark.parametrize('cls', (MappingKey, MappingValue)) +def test_mapping_not_a_map(cls): + m = cls('s') + assert list(m.match(yaml_compose('[foo]'))) == [] + + +def test_sequence_item_not_a_sequence(): + assert list(SequenceItem().match(yaml_compose('s: val'))) == [] + + +def test_mapping_key(): + m = MappingKey('s') + ret = [n.value for n in m.match(yaml_compose('s: val\nt: val2'))] + assert ret == ['s'] + + +def test_mapping_value(): + m = MappingValue('s') + ret = [n.value for n in m.match(yaml_compose('s: val\nt: val2'))] + assert ret == ['val'] + + +def test_sequence_item(): + ret = [n.value for n in SequenceItem().match(yaml_compose('[a, b, c]'))] + assert ret == ['a', 'b', 'c'] From 5679399d905a30b37c8132e8a854353f3025dcc3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 16 Sep 2024 20:36:33 -0400 Subject: [PATCH 894/967] migrate-config rewrites deprecated stages --- pre_commit/commands/migrate_config.py | 21 ++++++++++++++ tests/commands/migrate_config_test.py | 42 +++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index cdce83f54..ada094fa2 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -1,6 +1,7 @@ from __future__ import annotations import functools +import itertools import textwrap from typing import Callable @@ -49,6 +50,10 @@ def _preserve_style(n: ScalarNode, *, s: str) -> str: return f'{n.style}{s}{n.style}' +def _fix_stage(n: ScalarNode) -> str: + return _preserve_style(n, s=f'pre-{n.value}') + + def _migrate_composed(contents: str) -> str: tree = yaml_compose(contents) rewrites: list[tuple[ScalarNode, Callable[[ScalarNode], str]]] = [] @@ -76,6 +81,22 @@ def _migrate_composed(contents: str) -> str: if node.value == 'python_venv': rewrites.append((node, python_venv_replace)) + # stages rewrites + default_stages_matcher = (MappingValue('default_stages'), SequenceItem()) + default_stages_match = match(tree, default_stages_matcher) + hook_stages_matcher = ( + MappingValue('repos'), + SequenceItem(), + MappingValue('hooks'), + SequenceItem(), + MappingValue('stages'), + SequenceItem(), + ) + hook_stages_match = match(tree, hook_stages_matcher) + for node in itertools.chain(default_stages_match, hook_stages_match): + if node.value in {'commit', 'push', 'merge-commit'}: + rewrites.append((node, _fix_stage)) + rewrites.sort(reverse=True, key=lambda nf: nf[0].start_mark.index) src_parts = [] diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index c563866d9..9ffae6eef 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -213,6 +213,48 @@ def test_migrate_config_quoted_python_venv(tmp_path): assert cfg.read_text() == expected +def test_migrate_config_default_stages(tmp_path): + src = '''\ +default_stages: [commit, push, merge-commit, commit-msg] +repos: [] +''' + expected = '''\ +default_stages: [pre-commit, pre-push, pre-merge-commit, commit-msg] +repos: [] +''' + cfg = tmp_path.joinpath('cfg.yaml') + cfg.write_text(src) + assert migrate_config(str(cfg)) == 0 + assert cfg.read_text() == expected + + +def test_migrate_config_hook_stages(tmp_path): + src = '''\ +repos: +- repo: local + hooks: + - id: example + name: example + entry: example + language: system + stages: ["commit", "push", "merge-commit", "commit-msg"] +''' + expected = '''\ +repos: +- repo: local + hooks: + - id: example + name: example + entry: example + language: system + stages: ["pre-commit", "pre-push", "pre-merge-commit", "commit-msg"] +''' + cfg = tmp_path.joinpath('cfg.yaml') + cfg.write_text(src) + assert migrate_config(str(cfg)) == 0 + assert cfg.read_text() == expected + + def test_migrate_config_invalid_yaml(tmpdir): contents = '[' cfg = tmpdir.join(C.CONFIG_FILE) From a4e4cef335c62dc314fecbbd57e6fc57460c95d3 Mon Sep 17 00:00:00 2001 From: Travis Johnson Date: Thu, 9 May 2024 12:49:09 -0400 Subject: [PATCH 895/967] Upgrade to ruby-build v20240917 --- testing/make-archives | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/make-archives b/testing/make-archives index 3c7ab9dd0..251be4a58 100755 --- a/testing/make-archives +++ b/testing/make-archives @@ -17,7 +17,7 @@ from collections.abc import Sequence REPOS = ( ('rbenv', 'https://github.com/rbenv/rbenv', '38e1fbb'), - ('ruby-build', 'https://github.com/rbenv/ruby-build', '855b963'), + ('ruby-build', 'https://github.com/rbenv/ruby-build', 'ed384c8'), ( 'ruby-download', 'https://github.com/garnieretienne/rvm-download', From e687548842aab3c3ccc7677492960c740c2ced11 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Sep 2024 13:06:21 -0400 Subject: [PATCH 896/967] regenerate archives with python3.12 --- pre_commit/resources/rbenv.tar.gz | Bin 32551 -> 32545 bytes pre_commit/resources/ruby-download.tar.gz | Bin 5271 -> 5269 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/pre_commit/resources/rbenv.tar.gz b/pre_commit/resources/rbenv.tar.gz index da2514e71145e91debbad4bb1e11ca6d95ed3f63..111546e3dd9796511942c278495c21fe1ef947ae 100644 GIT binary patch delta 31950 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 delta 31979 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 delta 4959 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 From c9454e2ec3245b792eb7626e9bea799bf5e96bbf Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Sep 2024 13:07:55 -0400 Subject: [PATCH 897/967] regenerate ruby-build archive --- pre_commit/resources/ruby-build.tar.gz | Bin 75808 -> 88488 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz index 19d467fdd2867742cdefe402a78489481ce9abba..a4f7eb24fd0a7e4d59cf120014d8b054555b8b0f 100644 GIT binary patch literal 88488 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 aVnW&LI5^+h|Iiv<8JMAUgtgp` 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%0AtVc+vZ{$*a=M$ni`+p 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 Date: Mon, 30 Sep 2024 18:09:04 -0400 Subject: [PATCH 898/967] also apply sensible regex warning for `repo: meta` --- pre_commit/clientlib.py | 2 ++ tests/clientlib_test.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index a49465e89..0127c4d05 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -289,6 +289,8 @@ def check(self, dct: dict[str, Any]) -> None: item for item in MANIFEST_HOOK_DICT.items ), + OptionalSensibleRegexAtHook('files', cfgv.check_string), + OptionalSensibleRegexAtHook('exclude', cfgv.check_string), ) CONFIG_HOOK_DICT = cfgv.Map( 'Hook', 'id', diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index eaa8a044c..9d31d3399 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -256,6 +256,24 @@ def test_validate_optional_sensible_regex_at_local_hook(caplog): ] +def test_validate_optional_sensible_regex_at_meta_hook(caplog): + config_obj = { + 'repo': 'meta', + 'hooks': [{'id': 'identity', 'files': 'dir/*.py'}], + } + + cfgv.validate(config_obj, CONFIG_REPO_DICT) + + assert caplog.record_tuples == [ + ( + 'pre_commit', + logging.WARNING, + "The 'files' field in hook 'identity' is a regex, not a glob " + "-- matching '/*' probably isn't what you want here", + ), + ] + + @pytest.mark.parametrize( ('regex', 'warning'), ( From 7441a62eb1db5820d52a2c28afa3025773e4f015 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Sep 2024 18:41:13 -0400 Subject: [PATCH 899/967] add warning for deprecated stages names --- pre_commit/clientlib.py | 42 ++++++++++++++++++++++++++++++++++------- tests/clientlib_test.py | 26 +++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 0127c4d05..4e0425c33 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -99,6 +99,32 @@ def apply_default(self, dct: dict[str, Any]) -> None: super().apply_default(dct) +class DeprecatedStagesWarning(NamedTuple): + key: str + + def check(self, dct: dict[str, Any]) -> None: + if self.key not in dct: + return + + val = dct[self.key] + cfgv.check_array(cfgv.check_any)(val) + + legacy_stages = [stage for stage in val if stage in _STAGES] + if legacy_stages: + logger.warning( + f'hook id `{dct["id"]}` uses deprecated stage names ' + f'({", ".join(legacy_stages)}) which will be removed in a ' + f'future version. ' + f'run: `pre-commit migrate-config` to automatically fix this.', + ) + + def apply_default(self, dct: dict[str, Any]) -> None: + pass + + def remove_default(self, dct: dict[str, Any]) -> None: + raise NotImplementedError + + MANIFEST_HOOK_DICT = cfgv.Map( 'Hook', 'id', @@ -267,6 +293,12 @@ def check(self, dct: dict[str, Any]) -> None: raise cfgv.ValidationError(f'{self.key!r} cannot be overridden') +_COMMON_HOOK_WARNINGS = ( + OptionalSensibleRegexAtHook('files', cfgv.check_string), + OptionalSensibleRegexAtHook('exclude', cfgv.check_string), + DeprecatedStagesWarning('stages'), +) + META_HOOK_DICT = cfgv.Map( 'Hook', 'id', cfgv.Required('id', cfgv.check_string), @@ -289,8 +321,7 @@ def check(self, dct: dict[str, Any]) -> None: item for item in MANIFEST_HOOK_DICT.items ), - OptionalSensibleRegexAtHook('files', cfgv.check_string), - OptionalSensibleRegexAtHook('exclude', cfgv.check_string), + *_COMMON_HOOK_WARNINGS, ) CONFIG_HOOK_DICT = cfgv.Map( 'Hook', 'id', @@ -308,16 +339,13 @@ def check(self, dct: dict[str, Any]) -> None: if item.key != 'stages' ), StagesMigrationNoDefault('stages', []), - OptionalSensibleRegexAtHook('files', cfgv.check_string), - OptionalSensibleRegexAtHook('exclude', cfgv.check_string), + *_COMMON_HOOK_WARNINGS, ) LOCAL_HOOK_DICT = cfgv.Map( 'Hook', 'id', *MANIFEST_HOOK_DICT.items, - - OptionalSensibleRegexAtHook('files', cfgv.check_string), - OptionalSensibleRegexAtHook('exclude', cfgv.check_string), + *_COMMON_HOOK_WARNINGS, ) CONFIG_REPO_DICT = cfgv.Map( 'Repository', 'repo', diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 9d31d3399..1335e0868 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -309,6 +309,32 @@ def test_validate_optional_sensible_regex_at_top_level(caplog, regex, warning): assert caplog.record_tuples == [('pre_commit', logging.WARNING, warning)] +def test_warning_for_deprecated_stages(caplog): + config_obj = sample_local_config() + config_obj['hooks'][0]['stages'] = ['commit', 'push'] + + cfgv.validate(config_obj, CONFIG_REPO_DICT) + + assert caplog.record_tuples == [ + ( + 'pre_commit', + logging.WARNING, + 'hook id `do_not_commit` uses deprecated stage names ' + '(commit, push) which will be removed in a future version. ' + 'run: `pre-commit migrate-config` to automatically fix this.', + ), + ] + + +def test_no_warning_for_non_deprecated_stages(caplog): + config_obj = sample_local_config() + config_obj['hooks'][0]['stages'] = ['pre-commit', 'pre-push'] + + cfgv.validate(config_obj, CONFIG_REPO_DICT) + + assert caplog.record_tuples == [] + + @pytest.mark.parametrize( 'manifest_obj', ( From 33e020f315a0f8654500ffbbb103ef7b39fd7ff9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Sep 2024 19:22:14 -0400 Subject: [PATCH 900/967] add warning for deprecated stages values in `default_stages` --- pre_commit/clientlib.py | 27 +++++++++++++++++++++++++++ tests/clientlib_test.py | 24 ++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 4e0425c33..f78850718 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -125,6 +125,32 @@ def remove_default(self, dct: dict[str, Any]) -> None: raise NotImplementedError +class DeprecatedDefaultStagesWarning(NamedTuple): + key: str + + def check(self, dct: dict[str, Any]) -> None: + if self.key not in dct: + return + + val = dct[self.key] + cfgv.check_array(cfgv.check_any)(val) + + legacy_stages = [stage for stage in val if stage in _STAGES] + if legacy_stages: + logger.warning( + f'top-level `default_stages` uses deprecated stage names ' + f'({", ".join(legacy_stages)}) which will be removed in a ' + f'future version. ' + f'run: `pre-commit migrate-config` to automatically fix this.', + ) + + def apply_default(self, dct: dict[str, Any]) -> None: + pass + + def remove_default(self, dct: dict[str, Any]) -> None: + raise NotImplementedError + + MANIFEST_HOOK_DICT = cfgv.Map( 'Hook', 'id', @@ -398,6 +424,7 @@ def check(self, dct: dict[str, Any]) -> None: 'default_language_version', DEFAULT_LANGUAGE_VERSION, {}, ), StagesMigration('default_stages', STAGES), + DeprecatedDefaultStagesWarning('default_stages'), cfgv.Optional('files', check_string_regex, ''), cfgv.Optional('exclude', check_string_regex, '^$'), cfgv.Optional('fail_fast', cfgv.check_bool, False), diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 1335e0868..7aa84af0e 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -335,6 +335,30 @@ def test_no_warning_for_non_deprecated_stages(caplog): assert caplog.record_tuples == [] +def test_warning_for_deprecated_default_stages(caplog): + cfg = {'default_stages': ['commit', 'push'], 'repos': []} + + cfgv.validate(cfg, CONFIG_SCHEMA) + + assert caplog.record_tuples == [ + ( + 'pre_commit', + logging.WARNING, + 'top-level `default_stages` uses deprecated stage names ' + '(commit, push) which will be removed in a future version. ' + 'run: `pre-commit migrate-config` to automatically fix this.', + ), + ] + + +def test_no_warning_for_non_deprecated_default_stages(caplog): + cfg = {'default_stages': ['pre-commit', 'pre-push'], 'repos': []} + + cfgv.validate(cfg, CONFIG_SCHEMA) + + assert caplog.record_tuples == [] + + @pytest.mark.parametrize( 'manifest_obj', ( From 1d2f1c0ccea63906c8bcc9265bb9940383341c0c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Sep 2024 19:58:16 -0400 Subject: [PATCH 901/967] replace log_info_mock with pytest's caplog --- tests/conftest.py | 7 ------- tests/repository_test.py | 8 ++++---- tests/store_test.py | 8 ++++---- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index bd4af9a52..8c9cd14db 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,6 @@ import functools import io -import logging import os.path from unittest import mock @@ -203,12 +202,6 @@ def store(tempdir_factory): yield Store(os.path.join(tempdir_factory.get(), '.pre-commit')) -@pytest.fixture -def log_info_mock(): - with mock.patch.object(logging.getLogger('pre_commit'), 'info') as mck: - yield mck - - class Fixture: def __init__(self, stream: io.BytesIO) -> None: self._stream = stream diff --git a/tests/repository_test.py b/tests/repository_test.py index ac065ec40..32c361efa 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -240,16 +240,16 @@ def test_unknown_keys(store, caplog): assert msg == 'Unexpected key(s) present on local => too-much: foo, hello' -def test_reinstall(tempdir_factory, store, log_info_mock): +def test_reinstall(tempdir_factory, store, caplog): path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) _get_hook(config, store, 'foo') # We print some logging during clone (1) + install (3) - assert log_info_mock.call_count == 4 - log_info_mock.reset_mock() + assert len(caplog.record_tuples) == 4 + caplog.clear() # Reinstall on another run should not trigger another install _get_hook(config, store, 'foo') - assert log_info_mock.call_count == 0 + assert len(caplog.record_tuples) == 0 def test_control_c_control_c_on_install(tempdir_factory, store): diff --git a/tests/store_test.py b/tests/store_test.py index 45ec73272..b6b0a0b0f 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -65,7 +65,7 @@ def test_store_init(store): assert text_line in readme_contents -def test_clone(store, tempdir_factory, log_info_mock): +def test_clone(store, tempdir_factory, caplog): path = git_dir(tempdir_factory) with cwd(path): git_commit() @@ -74,7 +74,7 @@ def test_clone(store, tempdir_factory, log_info_mock): ret = store.clone(path, rev) # Should have printed some stuff - assert log_info_mock.call_args_list[0][0][0].startswith( + assert caplog.record_tuples[0][-1].startswith( 'Initializing environment for ', ) @@ -118,7 +118,7 @@ def test_clone_when_repo_already_exists(store): def test_clone_shallow_failure_fallback_to_complete( store, tempdir_factory, - log_info_mock, + caplog, ): path = git_dir(tempdir_factory) with cwd(path): @@ -134,7 +134,7 @@ def fake_shallow_clone(self, *args, **kwargs): ret = store.clone(path, rev) # Should have printed some stuff - assert log_info_mock.call_args_list[0][0][0].startswith( + assert caplog.record_tuples[0][-1].startswith( 'Initializing environment for ', ) From d31722386e57a98d8d7d6d74228d255b9a9ffaf3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Sep 2024 20:29:19 -0400 Subject: [PATCH 902/967] add warning for deprecates stages for remote repos on init --- pre_commit/clientlib.py | 38 +++++++++++++++++++++++ pre_commit/store.py | 5 +++ tests/store_test.py | 69 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index f78850718..c0f736d92 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -2,6 +2,7 @@ import functools import logging +import os.path import re import shlex import sys @@ -70,6 +71,43 @@ def transform_stage(stage: str) -> str: return _STAGES.get(stage, stage) +MINIMAL_MANIFEST_SCHEMA = cfgv.Array( + cfgv.Map( + 'Hook', 'id', + cfgv.Required('id', cfgv.check_string), + cfgv.Optional('stages', cfgv.check_array(cfgv.check_string), []), + ), +) + + +def warn_for_stages_on_repo_init(repo: str, directory: str) -> None: + try: + manifest = cfgv.load_from_filename( + os.path.join(directory, C.MANIFEST_FILE), + schema=MINIMAL_MANIFEST_SCHEMA, + load_strategy=yaml_load, + exc_tp=InvalidManifestError, + ) + except InvalidManifestError: + return # they'll get a better error message when it actually loads! + + legacy_stages = {} # sorted set + for hook in manifest: + for stage in hook.get('stages', ()): + if stage in _STAGES: + legacy_stages[stage] = True + + if legacy_stages: + logger.warning( + f'repo `{repo}` uses deprecated stage names ' + f'({", ".join(legacy_stages)}) which will be removed in a ' + f'future version. ' + f'Hint: often `pre-commit autoupdate --repo {shlex.quote(repo)}` ' + f'will fix this. ' + f'if it does not -- consider reporting an issue to that repo.', + ) + + class StagesMigrationNoDefault(NamedTuple): key: str default: Sequence[str] diff --git a/pre_commit/store.py b/pre_commit/store.py index 36cc49456..1235942c5 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -10,6 +10,7 @@ from typing import Callable import pre_commit.constants as C +from pre_commit import clientlib from pre_commit import file_lock from pre_commit import git from pre_commit.util import CalledProcessError @@ -136,6 +137,7 @@ def _new_repo( deps: Sequence[str], make_strategy: Callable[[str], None], ) -> str: + original_repo = repo repo = self.db_repo_name(repo, deps) def _get_result() -> str | None: @@ -168,6 +170,9 @@ def _get_result() -> str | None: 'INSERT INTO repos (repo, ref, path) VALUES (?, ?, ?)', [repo, ref, directory], ) + + clientlib.warn_for_stages_on_repo_init(original_repo, directory) + return directory def _complete_clone(self, ref: str, git_cmd: Callable[..., None]) -> None: diff --git a/tests/store_test.py b/tests/store_test.py index b6b0a0b0f..7d4dffb09 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -1,12 +1,15 @@ from __future__ import annotations +import logging import os.path +import shlex import sqlite3 import stat from unittest import mock import pytest +import pre_commit.constants as C from pre_commit import git from pre_commit.store import _get_default_directory from pre_commit.store import _LOCAL_RESOURCES @@ -91,6 +94,72 @@ def test_clone(store, tempdir_factory, caplog): assert store.select_all_repos() == [(path, rev, ret)] +def test_warning_for_deprecated_stages_on_init(store, tempdir_factory, caplog): + manifest = '''\ +- id: hook1 + name: hook1 + language: system + entry: echo hook1 + stages: [commit, push] +- id: hook2 + name: hook2 + language: system + entry: echo hook2 + stages: [push, merge-commit] +''' + + path = git_dir(tempdir_factory) + with open(os.path.join(path, C.MANIFEST_FILE), 'w') as f: + f.write(manifest) + cmd_output('git', 'add', '.', cwd=path) + git_commit(cwd=path) + rev = git.head_rev(path) + + store.clone(path, rev) + assert caplog.record_tuples[1] == ( + 'pre_commit', + logging.WARNING, + f'repo `{path}` uses deprecated stage names ' + f'(commit, push, merge-commit) which will be removed in a future ' + f'version. ' + f'Hint: often `pre-commit autoupdate --repo {shlex.quote(path)}` ' + f'will fix this. ' + f'if it does not -- consider reporting an issue to that repo.', + ) + + # should not re-warn + caplog.clear() + store.clone(path, rev) + assert caplog.record_tuples == [] + + +def test_no_warning_for_non_deprecated_stages_on_init( + store, tempdir_factory, caplog, +): + manifest = '''\ +- id: hook1 + name: hook1 + language: system + entry: echo hook1 + stages: [pre-commit, pre-push] +- id: hook2 + name: hook2 + language: system + entry: echo hook2 + stages: [pre-push, pre-merge-commit] +''' + + path = git_dir(tempdir_factory) + with open(os.path.join(path, C.MANIFEST_FILE), 'w') as f: + f.write(manifest) + cmd_output('git', 'add', '.', cwd=path) + git_commit(cwd=path) + rev = git.head_rev(path) + + store.clone(path, rev) + assert logging.WARNING not in {tup[1] for tup in caplog.record_tuples} + + def test_clone_cleans_up_on_checkout_failure(store): with pytest.raises(Exception) as excinfo: # This raises an exception because you can't clone something that From 801b956304e2ad2738bdb76d9c65ed52e967bb57 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 5 Oct 2024 13:30:25 -0400 Subject: [PATCH 903/967] remove deprecated python_venv alias --- pre_commit/all_languages.py | 2 -- pre_commit/repository.py | 9 --------- tests/all_languages_test.py | 7 ------- tests/repository_test.py | 18 ------------------ 4 files changed, 36 deletions(-) delete mode 100644 tests/all_languages_test.py diff --git a/pre_commit/all_languages.py b/pre_commit/all_languages.py index 476bad9da..f2d11bb60 100644 --- a/pre_commit/all_languages.py +++ b/pre_commit/all_languages.py @@ -44,7 +44,5 @@ 'script': script, 'swift': swift, 'system': system, - # TODO: fully deprecate `python_venv` - 'python_venv': python, } language_names = sorted(languages) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index aa8418563..a9461ab63 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -3,7 +3,6 @@ import json import logging import os -import shlex from collections.abc import Sequence from typing import Any @@ -68,14 +67,6 @@ def _hook_install(hook: Hook) -> None: logger.info('Once installed this environment will be reused.') logger.info('This may take a few minutes...') - if hook.language == 'python_venv': - logger.warning( - f'`repo: {hook.src}` uses deprecated `language: python_venv`. ' - f'This is an alias for `language: python`. ' - f'Often `pre-commit autoupdate --repo {shlex.quote(hook.src)}` ' - f'will fix this.', - ) - lang = languages[hook.language] assert lang.ENVIRONMENT_DIR is not None diff --git a/tests/all_languages_test.py b/tests/all_languages_test.py deleted file mode 100644 index 98c912150..000000000 --- a/tests/all_languages_test.py +++ /dev/null @@ -1,7 +0,0 @@ -from __future__ import annotations - -from pre_commit.all_languages import languages - - -def test_python_venv_is_an_alias_to_python(): - assert languages['python_venv'] is languages['python'] diff --git a/tests/repository_test.py b/tests/repository_test.py index 32c361efa..b54c910d3 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -80,24 +80,6 @@ def _test_hook_repo( assert out == expected -def test_python_venv_deprecation(store, caplog): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'example', - 'name': 'example', - 'language': 'python_venv', - 'entry': 'echo hi', - }], - } - _get_hook(config, store, 'example') - assert caplog.messages[-1] == ( - '`repo: local` uses deprecated `language: python_venv`. ' - 'This is an alias for `language: python`. ' - 'Often `pre-commit autoupdate --repo local` will fix this.' - ) - - def test_system_hook_with_spaces(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'system_hook_with_spaces_repo', From dbccd57db0e9cf993ea909e929eea97f6e4389ea Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 5 Oct 2024 14:58:22 -0400 Subject: [PATCH 904/967] v4.0.0 --- CHANGELOG.md | 25 +++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49094bbb9..2e4dd3cbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +4.0.0 - 2024-10-05 +================== + +### Features +- Improve `pre-commit migrate-config` to handle more yaml formats. + - #3301 PR by @asottile. +- Handle `stages` deprecation in `pre-commit migrate-config`. + - #3302 PR by @asottile. + - #2732 issue by @asottile. +- Upgrade `ruby-build`. + - #3199 PR by @ThisGuyCodes. +- Add "sensible regex" warnings to `repo: meta`. + - #3311 PR by @asottile. +- Add warnings for deprecated `stages` (`commit` -> `pre-commit`, `push` -> + `pre-push`, `merge-commit` -> `pre-merge-commit`). + - #3312 PR by @asottile. + - #3313 PR by @asottile. + - #3315 PR by @asottile. + - #2732 issue by @asottile. + +### Migrating +- `language: python_venv` has been removed -- use `language: python` instead. + - #3320 PR by @asottile. + - #2734 issue by @asottile. + 3.8.0 - 2024-07-28 ================== diff --git a/setup.cfg b/setup.cfg index 52b7681ef..70289e1fa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.8.0 +version = 4.0.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 4235a877f3ac4998b41e9cce8a709ac13de159b5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 00:02:26 +0000 Subject: [PATCH 905/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.6.0 → v5.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.6.0...v5.0.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 87b8551d4..7bd2611f8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer From 222c62bc5d2907efbd6052c5fb89c4c027400044 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 8 Oct 2024 11:46:48 -0400 Subject: [PATCH 906/967] fix migrate-config for purelib yaml --- pre_commit/commands/migrate_config.py | 3 ++- tests/commands/migrate_config_test.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index ada094fa2..c5d47a08e 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -47,7 +47,8 @@ def _migrate_map(contents: str) -> str: def _preserve_style(n: ScalarNode, *, s: str) -> str: - return f'{n.style}{s}{n.style}' + style = n.style or '' + return f'{style}{s}{style}' def _fix_stage(n: ScalarNode) -> str: diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index 9ffae6eef..a517d2f44 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -1,10 +1,26 @@ from __future__ import annotations +from unittest import mock + import pytest +import yaml import pre_commit.constants as C from pre_commit.clientlib import InvalidConfigError from pre_commit.commands.migrate_config import migrate_config +from pre_commit.yaml import yaml_compose + + +@pytest.fixture(autouse=True, params=['c', 'pure']) +def switch_pyyaml_impl(request): + if request.param == 'c': + yield + else: + with mock.patch.dict( + yaml_compose.keywords, + {'Loader': yaml.SafeLoader}, + ): + yield def test_migrate_config_normal_format(tmpdir, capsys): From cc4a52241565440ce200666799eef70626457488 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 8 Oct 2024 12:08:49 -0400 Subject: [PATCH 907/967] v4.0.1 --- CHANGELOG.md | 9 +++++++++ setup.cfg | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e4dd3cbb..a9b4c8c22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +4.0.1 - 2024-10-08 +================== + +### Fixes +- Fix `pre-commit migrate-config` for unquoted deprecated stages names with + purelib `pyyaml`. + - #3324 PR by @asottile. + - pre-commit-ci/issues#234 issue by @lorenzwalthert. + 4.0.0 - 2024-10-05 ================== diff --git a/setup.cfg b/setup.cfg index 70289e1fa..6936a1f0d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 4.0.0 +version = 4.0.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 46de4da34e362e8dfa34c08205b662da8ab47788 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 22:30:38 +0000 Subject: [PATCH 908/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v2.5.0 → v2.7.0](https://github.com/asottile/setup-cfg-fmt/compare/v2.5.0...v2.7.0) - [github.com/asottile/reorder-python-imports: v3.13.0 → v3.14.0](https://github.com/asottile/reorder-python-imports/compare/v3.13.0...v3.14.0) - [github.com/asottile/pyupgrade: v3.17.0 → v3.18.0](https://github.com/asottile/pyupgrade/compare/v3.17.0...v3.18.0) - [github.com/pre-commit/mirrors-mypy: v1.11.2 → v1.12.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.11.2...v1.12.1) --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7bd2611f8..33c905cd4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,11 +10,11 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.5.0 + rev: v2.7.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports - rev: v3.13.0 + rev: v3.14.0 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.17.0 + rev: v3.18.0 hooks: - id: pyupgrade args: [--py39-plus] @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.11.2 + rev: v1.12.1 hooks: - id: mypy additional_dependencies: [types-pyyaml] From 0de4c8028a95c1a7dfd57e772ec11ce3a71834cc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 21 Oct 2024 20:35:56 -0400 Subject: [PATCH 909/967] remove unused type ignore --- testing/make-archives | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/testing/make-archives b/testing/make-archives index 251be4a58..eb3f3af85 100755 --- a/testing/make-archives +++ b/testing/make-archives @@ -57,8 +57,7 @@ def make_archive(name: str, repo: str, ref: str, destdir: str) -> str: arcs.sort() with gzip.GzipFile(output_path, 'wb', mtime=0) as gzipf: - # https://github.com/python/typeshed/issues/5491 - with tarfile.open(fileobj=gzipf, mode='w') as tf: # type: ignore + with tarfile.open(fileobj=gzipf, mode='w') as tf: for arcname, abspath in arcs: tf.add( abspath, From 708ca3b581f3fa033d918dd6d5b3794803d4dbb2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 22:56:52 +0000 Subject: [PATCH 910/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.18.0 → v3.19.0](https://github.com/asottile/pyupgrade/compare/v3.18.0...v3.19.0) - [github.com/pre-commit/mirrors-mypy: v1.12.1 → v1.13.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.12.1...v1.13.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 33c905cd4..614170ba7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.18.0 + rev: v3.19.0 hooks: - id: pyupgrade args: [--py39-plus] @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.12.1 + rev: v1.13.0 hooks: - id: mypy additional_dependencies: [types-pyyaml] From 85783bdc0ba86c3e772612a44b8825de1d24a6da Mon Sep 17 00:00:00 2001 From: Fredrik Ekre Date: Fri, 1 Nov 2024 15:24:51 +0100 Subject: [PATCH 911/967] Add support for julia hooks This patch adds 2nd class support for hooks using julia as the language. pre-commit will install any dependencies defined in the hooks repo `Project.toml` file, with support for `additional_dependencies` as well. Julia doesn't (yet) have a way to install binaries/scripts so for julia hooks the `entry` value is a (relative) path to a julia script within the hooks repository. When executing a julia hook the (globally installed) julia interpreter is prepended to the entry. Example `.pre-commit-hooks.yaml`: ```yaml - id: foo name: ... language: julia entry: bin/foo.jl --arg1 ``` Example hooks repo: https://github.com/fredrikekre/runic-pre-commit/tree/fe/julia Accompanying pre-commit.com PR: https://github.com/pre-commit/pre-commit.com/pull/998 Fixes #2689. --- pre_commit/all_languages.py | 2 + pre_commit/languages/julia.py | 132 ++++++++++++++++++++++++++++++++++ tests/languages/julia_test.py | 97 +++++++++++++++++++++++++ 3 files changed, 231 insertions(+) create mode 100644 pre_commit/languages/julia.py create mode 100644 tests/languages/julia_test.py diff --git a/pre_commit/all_languages.py b/pre_commit/all_languages.py index f2d11bb60..ba569c377 100644 --- a/pre_commit/all_languages.py +++ b/pre_commit/all_languages.py @@ -10,6 +10,7 @@ from pre_commit.languages import fail from pre_commit.languages import golang from pre_commit.languages import haskell +from pre_commit.languages import julia from pre_commit.languages import lua from pre_commit.languages import node from pre_commit.languages import perl @@ -33,6 +34,7 @@ 'fail': fail, 'golang': golang, 'haskell': haskell, + 'julia': julia, 'lua': lua, 'node': node, 'perl': perl, diff --git a/pre_commit/languages/julia.py b/pre_commit/languages/julia.py new file mode 100644 index 000000000..df91c0697 --- /dev/null +++ b/pre_commit/languages/julia.py @@ -0,0 +1,132 @@ +from __future__ import annotations + +import contextlib +import os +import shutil +from collections.abc import Generator +from collections.abc import Sequence + +from pre_commit import lang_base +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import UNSET +from pre_commit.prefix import Prefix +from pre_commit.util import cmd_output_b + +ENVIRONMENT_DIR = 'juliaenv' +health_check = lang_base.basic_health_check +get_default_version = lang_base.basic_get_default_version + + +def run_hook( + prefix: Prefix, + entry: str, + args: Sequence[str], + file_args: Sequence[str], + *, + is_local: bool, + require_serial: bool, + color: bool, +) -> tuple[int, bytes]: + # `entry` is a (hook-repo relative) file followed by (optional) args, e.g. + # `bin/id.jl` or `bin/hook.jl --arg1 --arg2` so we + # 1) shell parse it and join with args with hook_cmd + # 2) prepend the hooks prefix path to the first argument (the file), unless + # it is a local script + # 3) prepend `julia` as the interpreter + + cmd = lang_base.hook_cmd(entry, args) + script = cmd[0] if is_local else prefix.path(cmd[0]) + cmd = ('julia', script, *cmd[1:]) + return lang_base.run_xargs( + cmd, + file_args, + require_serial=require_serial, + color=color, + ) + + +def get_env_patch(target_dir: str, version: str) -> PatchesT: + return ( + ('JULIA_LOAD_PATH', target_dir), + # May be set, remove it to not interfer with LOAD_PATH + ('JULIA_PROJECT', UNSET), + ) + + +@contextlib.contextmanager +def in_env(prefix: Prefix, version: str) -> Generator[None]: + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) + with envcontext(get_env_patch(envdir, version)): + yield + + +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) + with in_env(prefix, version): + # TODO: Support language_version with juliaup similar to rust via + # rustup + # if version != 'system': + # ... + + # Copy Project.toml to hook env if it exist + os.makedirs(envdir, exist_ok=True) + project_names = ('JuliaProject.toml', 'Project.toml') + project_found = False + for project_name in project_names: + project_file = prefix.path(project_name) + if not os.path.isfile(project_file): + continue + shutil.copy(project_file, envdir) + project_found = True + break + + # If no project file was found we create an empty one so that the + # package manager doesn't error + if not project_found: + open(os.path.join(envdir, 'Project.toml'), 'a').close() + + # Copy Manifest.toml to hook env if it exists + manifest_names = ('JuliaManifest.toml', 'Manifest.toml') + for manifest_name in manifest_names: + manifest_file = prefix.path(manifest_name) + if not os.path.isfile(manifest_file): + continue + shutil.copy(manifest_file, envdir) + break + + # Julia code to instantiate the hook environment + julia_code = """ + @assert length(ARGS) > 0 + hook_env = ARGS[1] + deps = join(ARGS[2:end], " ") + + # We prepend @stdlib here so that we can load the package manager even + # though `get_env_patch` limits `JULIA_LOAD_PATH` to just the hook env. + pushfirst!(LOAD_PATH, "@stdlib") + using Pkg + popfirst!(LOAD_PATH) + + # Instantiate the environment shipped with the hook repo. If we have + # additional dependencies we disable precompilation in this step to + # avoid double work. + precompile = isempty(deps) ? "1" : "0" + withenv("JULIA_PKG_PRECOMPILE_AUTO" => precompile) do + Pkg.instantiate() + end + + # Add additional dependencies (with precompilation) + if !isempty(deps) + withenv("JULIA_PKG_PRECOMPILE_AUTO" => "1") do + Pkg.REPLMode.pkgstr("add " * deps) + end + end + """ + cmd_output_b( + 'julia', '-e', julia_code, '--', envdir, *additional_dependencies, + cwd=prefix.prefix_dir, + ) diff --git a/tests/languages/julia_test.py b/tests/languages/julia_test.py new file mode 100644 index 000000000..4ea3c25b2 --- /dev/null +++ b/tests/languages/julia_test.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from pre_commit.languages import julia +from testing.language_helpers import run_language +from testing.util import cwd + + +def _make_hook(tmp_path, julia_code): + src_dir = tmp_path.joinpath('src') + src_dir.mkdir() + src_dir.joinpath('main.jl').write_text(julia_code) + tmp_path.joinpath('Project.toml').write_text( + '[deps]\n' + 'Example = "7876af07-990d-54b4-ab0e-23690620f79a"\n', + ) + + +def test_julia_hook(tmp_path): + code = """ + using Example + function main() + println("Hello, world!") + end + main() + """ + _make_hook(tmp_path, code) + expected = (0, b'Hello, world!\n') + assert run_language(tmp_path, julia, 'src/main.jl') == expected + + +def test_julia_hook_manifest(tmp_path): + code = """ + using Example + println(pkgversion(Example)) + """ + _make_hook(tmp_path, code) + + tmp_path.joinpath('Manifest.toml').write_text( + 'manifest_format = "2.0"\n\n' + '[[deps.Example]]\n' + 'git-tree-sha1 = "11820aa9c229fd3833d4bd69e5e75ef4e7273bf1"\n' + 'uuid = "7876af07-990d-54b4-ab0e-23690620f79a"\n' + 'version = "0.5.4"\n', + ) + expected = (0, b'0.5.4\n') + assert run_language(tmp_path, julia, 'src/main.jl') == expected + + +def test_julia_hook_args(tmp_path): + code = """ + function main(argv) + foreach(println, argv) + end + main(ARGS) + """ + _make_hook(tmp_path, code) + expected = (0, b'--arg1\n--arg2\n') + assert run_language( + tmp_path, julia, 'src/main.jl --arg1 --arg2', + ) == expected + + +def test_julia_hook_additional_deps(tmp_path): + code = """ + using TOML + function main() + project_file = Base.active_project() + dict = TOML.parsefile(project_file) + for (k, v) in dict["deps"] + println(k, " = ", v) + end + end + main() + """ + _make_hook(tmp_path, code) + deps = ('TOML=fa267f1f-6049-4f14-aa54-33bafae1ed76',) + ret, out = run_language(tmp_path, julia, 'src/main.jl', deps=deps) + assert ret == 0 + assert b'Example = 7876af07-990d-54b4-ab0e-23690620f79a' in out + assert b'TOML = fa267f1f-6049-4f14-aa54-33bafae1ed76' in out + + +def test_julia_repo_local(tmp_path): + env_dir = tmp_path.joinpath('envdir') + env_dir.mkdir() + local_dir = tmp_path.joinpath('local') + local_dir.mkdir() + local_dir.joinpath('local.jl').write_text( + 'using TOML; foreach(println, ARGS)', + ) + with cwd(local_dir): + deps = ('TOML=fa267f1f-6049-4f14-aa54-33bafae1ed76',) + expected = (0, b'--local-arg1\n--local-arg2\n') + assert run_language( + env_dir, julia, 'local.jl --local-arg1 --local-arg2', + deps=deps, is_local=True, + ) == expected From 109628c5058e6901cc69a1d0dfa3c2a0e0ea14d8 Mon Sep 17 00:00:00 2001 From: AleksaC Date: Thu, 19 Sep 2024 01:01:33 +0200 Subject: [PATCH 912/967] disable automatic toolchain switching for golang hooks --- pre_commit/languages/golang.py | 2 + tests/languages/golang_test.py | 69 ++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 609087962..678c04b14 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -75,6 +75,7 @@ def get_env_patch(venv: str, version: str) -> PatchesT: return ( ('GOROOT', os.path.join(venv, '.go')), + ('GOTOOLCHAIN', 'local'), ( 'PATH', ( os.path.join(venv, 'bin'), os.pathsep, @@ -145,6 +146,7 @@ def install_environment( env = no_git_env(dict(os.environ, GOPATH=gopath)) env.pop('GOBIN', None) if version != 'system': + env['GOTOOLCHAIN'] = 'local' env['GOROOT'] = os.path.join(env_dir, '.go') env['PATH'] = os.pathsep.join(( os.path.join(env_dir, '.go', 'bin'), os.environ['PATH'], diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py index 02e35d710..7fb6ab18b 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -11,11 +11,13 @@ from pre_commit.envcontext import envcontext from pre_commit.languages import golang from pre_commit.store import _make_local_repo +from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from testing.fixtures import add_config_to_repo from testing.fixtures import make_config_from_repo from testing.language_helpers import run_language from testing.util import cmd_output_mocked_pre_commit_home +from testing.util import cwd from testing.util import git_commit @@ -165,3 +167,70 @@ def test_during_commit_all(tmp_path, tempdir_factory, store, in_git_dir): fn=cmd_output_mocked_pre_commit_home, tempdir_factory=tempdir_factory, ) + + +def test_automatic_toolchain_switching(tmp_path): + go_mod = '''\ +module toolchain-version-test + +go 1.23.1 +''' + main_go = '''\ +package main + +func main() {} +''' + tmp_path.joinpath('go.mod').write_text(go_mod) + mod_dir = tmp_path.joinpath('toolchain-version-test') + mod_dir.mkdir() + main_file = mod_dir.joinpath('main.go') + main_file.write_text(main_go) + + with pytest.raises(CalledProcessError) as excinfo: + run_language( + path=tmp_path, + language=golang, + version='1.22.0', + exe='golang-version-test', + ) + + assert 'go.mod requires go >= 1.23.1' in excinfo.value.stderr.decode() + + +def test_automatic_toolchain_switching_go_fmt(tmp_path, monkeypatch): + go_mod_hook = '''\ +module toolchain-version-test + +go 1.22.0 +''' + go_mod = '''\ +module toolchain-version-test + +go 1.23.1 +''' + main_go = '''\ +package main + +func main() {} +''' + hook_dir = tmp_path.joinpath('hook') + hook_dir.mkdir() + hook_dir.joinpath('go.mod').write_text(go_mod_hook) + + test_dir = tmp_path.joinpath('test') + test_dir.mkdir() + test_dir.joinpath('go.mod').write_text(go_mod) + main_file = test_dir.joinpath('main.go') + main_file.write_text(main_go) + + with cwd(test_dir): + ret, out = run_language( + path=hook_dir, + language=golang, + version='1.22.0', + exe='go fmt', + file_args=(str(main_file),), + ) + + assert ret == 1 + assert 'go.mod requires go >= 1.23.1' in out.decode() From db85eeed2d114b1fb60cc6c969573fefa23c4fc8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 22:45:24 +0000 Subject: [PATCH 913/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.19.0 → v3.19.1](https://github.com/asottile/pyupgrade/compare/v3.19.0...v3.19.1) - [github.com/pre-commit/mirrors-mypy: v1.13.0 → v1.14.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.13.0...v1.14.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 614170ba7..5743224e8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.19.0 + rev: v3.19.1 hooks: - id: pyupgrade args: [--py39-plus] @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.13.0 + rev: v1.14.0 hooks: - id: mypy additional_dependencies: [types-pyyaml] From aa85be934071e7c73fb49e9339e307285a784d16 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Dec 2024 15:43:55 -0500 Subject: [PATCH 914/967] fix docker_image test when ubuntu:22.04 image is not pre-pulled --- tests/languages/docker_image_test.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/languages/docker_image_test.py b/tests/languages/docker_image_test.py index 4e3a8789a..4f720600b 100644 --- a/tests/languages/docker_image_test.py +++ b/tests/languages/docker_image_test.py @@ -1,10 +1,18 @@ from __future__ import annotations +import pytest + from pre_commit.languages import docker_image +from pre_commit.util import cmd_output_b from testing.language_helpers import run_language from testing.util import xfailif_windows +@pytest.fixture(autouse=True, scope='module') +def _ensure_image_available(): + cmd_output_b('docker', 'run', '--rm', 'ubuntu:22.04', 'echo') + + @xfailif_windows # pragma: win32 no cover def test_docker_image_hook_via_entrypoint(tmp_path): ret = run_language( From 28c3d81bd27fe5e62eead459c1963a582e763bd7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Dec 2024 15:50:58 -0500 Subject: [PATCH 915/967] update .net tests to use .net 8 .net 6 and 7 were removed from github actions runners --- tests/languages/dotnet_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/languages/dotnet_test.py b/tests/languages/dotnet_test.py index 470c03b22..ee4082568 100644 --- a/tests/languages/dotnet_test.py +++ b/tests/languages/dotnet_test.py @@ -27,7 +27,7 @@ def _csproj(tool_name): Exe - net6 + net8 true {tool_name} ./nupkg From 77edad8455e88b403e055d2692c9545085cf3edb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Dec 2024 16:06:00 -0500 Subject: [PATCH 916/967] install r on ubuntu runners this was removed in ubuntu-24.04 github actions runner --- .github/workflows/languages.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/languages.yaml b/.github/workflows/languages.yaml index 7d50535f8..61293a0d8 100644 --- a/.github/workflows/languages.yaml +++ b/.github/workflows/languages.yaml @@ -65,6 +65,8 @@ jobs: if: matrix.os == 'windows-latest' && matrix.language == 'perl' - uses: haskell/actions/setup@v2 if: matrix.language == 'haskell' + - uses: r-lib/actions/setup-r@v2 + if: matrix.os == 'ubuntu-latest' && matrix.language == 'r' - name: install deps run: python -mpip install -e . -r requirements-dev.txt From 9b9f8e254d46da65c8544244c423596d54260e24 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 23:30:19 +0000 Subject: [PATCH 917/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.14.0 → v1.14.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.14.0...v1.14.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5743224e8..4a23da2bc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.14.0 + rev: v1.14.1 hooks: - id: mypy additional_dependencies: [types-pyyaml] From c2c061cf63e00a3ff8c88a9054c47e96a36f2daa Mon Sep 17 00:00:00 2001 From: Lorenz Walthert Date: Sun, 19 Jan 2025 19:43:24 +0100 Subject: [PATCH 918/967] fix: ensure env patch is applied for vanilla emulation otherwise, installing the hooks when RENV_USER env variable is set (e.g. in RStudio with renv project) will result in executing the installation script in the wrong renv --- pre_commit/languages/r.py | 48 +++++++++++++++++++++++++++++---------- tests/languages/r_test.py | 2 +- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index c75a30893..f70d2fdca 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -15,27 +15,50 @@ from pre_commit.envcontext import UNSET from pre_commit.prefix import Prefix from pre_commit.util import cmd_output -from pre_commit.util import cmd_output_b from pre_commit.util import win_exe ENVIRONMENT_DIR = 'renv' -RSCRIPT_OPTS = ('--no-save', '--no-restore', '--no-site-file', '--no-environ') get_default_version = lang_base.basic_get_default_version +_RENV_ACTIVATED_OPTS = ( + '--no-save', '--no-restore', '--no-site-file', '--no-environ', +) -def _execute_vanilla_r_code_as_script( + +def _execute_r( code: str, *, prefix: Prefix, version: str, args: Sequence[str] = (), cwd: str, + cli_opts: Sequence[str], ) -> str: with in_env(prefix, version), _r_code_in_tempfile(code) as f: _, out, _ = cmd_output( - _rscript_exec(), *RSCRIPT_OPTS, f, *args, cwd=cwd, + _rscript_exec(), *cli_opts, f, *args, cwd=cwd, ) return out.rstrip('\n') +def _execute_r_in_renv( + code: str, *, + prefix: Prefix, version: str, args: Sequence[str] = (), cwd: str, +) -> str: + return _execute_r( + code=code, prefix=prefix, version=version, args=args, cwd=cwd, + cli_opts=_RENV_ACTIVATED_OPTS, + ) + + +def _execute_vanilla_r( + code: str, *, + prefix: Prefix, version: str, args: Sequence[str] = (), cwd: str, +) -> str: + return _execute_r( + code=code, prefix=prefix, version=version, args=args, cwd=cwd, + cli_opts=('--vanilla',), + ) + + def _read_installed_version(envdir: str, prefix: Prefix, version: str) -> str: - return _execute_vanilla_r_code_as_script( + return _execute_r_in_renv( 'cat(renv::settings$r.version())', prefix=prefix, version=version, cwd=envdir, @@ -43,7 +66,7 @@ def _read_installed_version(envdir: str, prefix: Prefix, version: str) -> str: def _read_executable_version(envdir: str, prefix: Prefix, version: str) -> str: - return _execute_vanilla_r_code_as_script( + return _execute_r_in_renv( 'cat(as.character(getRversion()))', prefix=prefix, version=version, cwd=envdir, @@ -53,7 +76,7 @@ def _read_executable_version(envdir: str, prefix: Prefix, version: str) -> str: def _write_current_r_version( envdir: str, prefix: Prefix, version: str, ) -> None: - _execute_vanilla_r_code_as_script( + _execute_r_in_renv( 'renv::settings$r.version(as.character(getRversion()))', prefix=prefix, version=version, cwd=envdir, @@ -161,7 +184,7 @@ def _cmd_from_hook( _entry_validate(cmd) cmd_part = _prefix_if_file_entry(cmd, prefix, is_local=is_local) - return (cmd[0], *RSCRIPT_OPTS, *cmd_part, *args) + return (cmd[0], *_RENV_ACTIVATED_OPTS, *cmd_part, *args) def install_environment( @@ -204,14 +227,15 @@ def install_environment( renv::install(prefix_dir) }} """ - - with _r_code_in_tempfile(r_code_inst_environment) as f: - cmd_output_b(_rscript_exec(), '--vanilla', f, cwd=env_dir) + _execute_vanilla_r( + r_code_inst_environment, + prefix=prefix, version=version, cwd=env_dir, + ) _write_current_r_version(envdir=env_dir, prefix=prefix, version=version) if additional_dependencies: r_code_inst_add = 'renv::install(commandArgs(trailingOnly = TRUE))' - _execute_vanilla_r_code_as_script( + _execute_r_in_renv( code=r_code_inst_add, prefix=prefix, version=version, args=additional_dependencies, cwd=env_dir, diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py index 10919e4a7..9e73129e1 100644 --- a/tests/languages/r_test.py +++ b/tests/languages/r_test.py @@ -286,7 +286,7 @@ def test_health_check_without_version(prefix, installed_environment, version): prefix, env_dir = installed_environment # simulate old pre-commit install by unsetting the installed version - r._execute_vanilla_r_code_as_script( + r._execute_r_in_renv( f'renv::settings$r.version({version})', prefix=prefix, version=C.DEFAULT, cwd=env_dir, ) From b152e922ef11a97efe22ca7dc4f90011f0d1711c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Jan 2025 13:35:33 -0500 Subject: [PATCH 919/967] v4.1.0 --- CHANGELOG.md | 18 ++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9b4c8c22..408afe68b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +4.1.0 - 2025-01-20 +================== + +### Features +- Add `language: julia`. + - #3348 PR by @fredrikekre. + - #2689 issue @jmuchovej. + +### Fixes +- Disable automatic toolchain switching for `language: golang`. + - #3304 PR by @AleksaC. + - #3300 issue by @AleksaC. + - #3149 issue by @nijel. +- Fix `language: r` installation when initiated by RStudio. + - #3389 PR by @lorenzwalthert. + - #3385 issue by @lorenzwalthert. + + 4.0.1 - 2024-10-08 ================== diff --git a/setup.cfg b/setup.cfg index 6936a1f0d..60d976418 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 4.0.1 +version = 4.1.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From edd0002e4312dc62fc8a236a3b4dc08d1012555d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2025 22:30:07 +0000 Subject: [PATCH 920/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/hhatto/autopep8: v2.3.1 → v2.3.2](https://github.com/hhatto/autopep8/compare/v2.3.1...v2.3.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4a23da2bc..b73622927 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/hhatto/autopep8 - rev: v2.3.1 + rev: v2.3.2 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 From e2210c97e2128703e41cc19e66f24c23b9157f69 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 30 Jan 2025 14:58:50 -0500 Subject: [PATCH 921/967] upgrade asottile/workflows Committed via https://github.com/asottile/all-repos --- .github/workflows/languages.yaml | 2 +- .github/workflows/main.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/languages.yaml b/.github/workflows/languages.yaml index 61293a0d8..fccf29892 100644 --- a/.github/workflows/languages.yaml +++ b/.github/workflows/languages.yaml @@ -36,7 +36,7 @@ jobs: matrix: include: ${{ fromJSON(needs.vars.outputs.languages) }} steps: - - uses: asottile/workflows/.github/actions/fast-checkout@v1.4.0 + - uses: asottile/workflows/.github/actions/fast-checkout@v1.8.1 - uses: actions/setup-python@v4 with: python-version: 3.9 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2355b6620..7fda646ff 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,12 +12,12 @@ concurrency: jobs: main-windows: - uses: asottile/workflows/.github/workflows/tox.yml@v1.6.0 + uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1 with: env: '["py39"]' os: windows-latest main-linux: - uses: asottile/workflows/.github/workflows/tox.yml@v1.6.0 + uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1 with: env: '["py39", "py310", "py311", "py312"]' os: ubuntu-latest From 4f90a1e88a80dd460f36e21d774d06bf0e73921b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 22:44:01 +0000 Subject: [PATCH 922/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.14.1 → v1.15.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.14.1...v1.15.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b73622927..ead07d89e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.14.1 + rev: v1.15.0 hooks: - id: mypy additional_dependencies: [types-pyyaml] From 94b97e28f7cc7d9bcb536d7a3cf7ef6311e076fd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 21:07:26 +0000 Subject: [PATCH 923/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 7.1.1 → 7.1.2](https://github.com/PyCQA/flake8/compare/7.1.1...7.1.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ead07d89e..b216fbd69 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,7 +33,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 - rev: 7.1.1 + rev: 7.1.2 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy From b7eb412c798424a94ca83c72eed6f97271545dc4 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Thu, 13 Mar 2025 17:29:20 +0530 Subject: [PATCH 924/967] fix: crash on ambiguous ref 'HEAD' --- pre_commit/git.py | 2 +- tests/git_test.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 19aac3872..2f424f89e 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -126,7 +126,7 @@ def get_conflicted_files() -> set[str]: merge_diff_filenames = zsplit( cmd_output( 'git', 'diff', '--name-only', '--no-ext-diff', '-z', - '-m', tree_hash, 'HEAD', 'MERGE_HEAD', + '-m', tree_hash, 'HEAD', 'MERGE_HEAD', '--', )[1], ) return set(merge_conflict_filenames) | set(merge_diff_filenames) diff --git a/tests/git_test.py b/tests/git_test.py index 93f5a1c6e..02b6ce3ae 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -141,6 +141,15 @@ def test_get_conflicted_files_unstaged_files(in_merge_conflict): assert ret == {'conflict_file'} +def test_get_conflicted_files_with_file_named_head(in_merge_conflict): + resolve_conflict() + open('HEAD', 'w').close() + cmd_output('git', 'add', 'HEAD') + + ret = set(git.get_conflicted_files()) + assert ret == {'conflict_file', 'HEAD'} + + MERGE_MSG = b"Merge branch 'foo' into bar\n\nConflicts:\n\tconflict_file\n" OTHER_MERGE_MSG = MERGE_MSG + b'\tother_conflict_file\n' From 3e8d0f5e1c449381272b80241140e985631f9912 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 18 Mar 2025 14:55:24 -0400 Subject: [PATCH 925/967] adjust python default_language_version to prefer versioned exe --- pre_commit/languages/python.py | 38 +++++++++++++------ tests/languages/python_test.py | 67 ++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 12 deletions(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 0c4bb62db..88ececce6 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -75,6 +75,13 @@ def _find_by_py_launcher( return None +def _impl_exe_name() -> str: + if sys.implementation.name == 'cpython': # pragma: cpython cover + return 'python' + else: # pragma: cpython no cover + return sys.implementation.name # pypy mostly + + def _find_by_sys_executable() -> str | None: def _norm(path: str) -> str | None: _, exe = os.path.split(path.lower()) @@ -100,18 +107,25 @@ def _norm(path: str) -> str | None: @functools.lru_cache(maxsize=1) def get_default_version() -> str: # pragma: no cover (platform dependent) - # First attempt from `sys.executable` (or the realpath) - exe = _find_by_sys_executable() - if exe: - return exe - - # Next try the `pythonX.X` executable - exe = f'python{sys.version_info[0]}.{sys.version_info[1]}' - if find_executable(exe): - return exe - - if _find_by_py_launcher(exe): - return exe + v_major = f'{sys.version_info[0]}' + v_minor = f'{sys.version_info[0]}.{sys.version_info[1]}' + + # attempt the likely implementation exe + for potential in (v_minor, v_major): + exe = f'{_impl_exe_name()}{potential}' + if find_executable(exe): + return exe + + # next try `sys.executable` (or the realpath) + maybe_exe = _find_by_sys_executable() + if maybe_exe: + return maybe_exe + + # maybe on windows we can find it via py launcher? + if sys.platform == 'win32': # pragma: win32 cover + exe = f'python{v_minor}' + if _find_by_py_launcher(exe): + return exe # We tried! return C.DEFAULT diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index ab26e14e7..565525a40 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -12,6 +12,7 @@ from pre_commit.prefix import Prefix from pre_commit.util import make_executable from pre_commit.util import win_exe +from testing.auto_namedtuple import auto_namedtuple from testing.language_helpers import run_language @@ -34,6 +35,72 @@ def test_read_pyvenv_cfg_non_utf8(tmpdir): assert python._read_pyvenv_cfg(pyvenv_cfg) == expected +def _get_default_version( + *, + impl: str, + exe: str, + found: set[str], + version: tuple[int, int], +) -> str: + sys_exe = f'/fake/path/{exe}' + sys_impl = auto_namedtuple(name=impl) + sys_ver = auto_namedtuple(major=version[0], minor=version[1]) + + def find_exe(s): + if s in found: + return f'/fake/path/found/{exe}' + else: + return None + + with ( + mock.patch.object(sys, 'implementation', sys_impl), + mock.patch.object(sys, 'executable', sys_exe), + mock.patch.object(sys, 'version_info', sys_ver), + mock.patch.object(python, 'find_executable', find_exe), + ): + return python.get_default_version.__wrapped__() + + +def test_default_version_sys_executable_found(): + ret = _get_default_version( + impl='cpython', + exe='python3.12', + found={'python3.12'}, + version=(3, 12), + ) + assert ret == 'python3.12' + + +def test_default_version_picks_specific_when_found(): + ret = _get_default_version( + impl='cpython', + exe='python3', + found={'python3', 'python3.12'}, + version=(3, 12), + ) + assert ret == 'python3.12' + + +def test_default_version_picks_pypy_versioned_exe(): + ret = _get_default_version( + impl='pypy', + exe='python', + found={'pypy3.12', 'python3'}, + version=(3, 12), + ) + assert ret == 'pypy3.12' + + +def test_default_version_picks_pypy_unversioned_exe(): + ret = _get_default_version( + impl='pypy', + exe='python', + found={'pypy3', 'python3'}, + version=(3, 12), + ) + assert ret == 'pypy3' + + def test_norm_version_expanduser(): home = os.path.expanduser('~') if sys.platform == 'win32': # pragma: win32 cover From aa48766b888990e7b118d12cf757109d96e65a7e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 18 Mar 2025 17:34:49 -0400 Subject: [PATCH 926/967] v4.2.0 --- CHANGELOG.md | 13 +++++++++++++ setup.cfg | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 408afe68b..b63f44317 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +4.2.0 - 2025-03-18 +================== + +### Features +- For `language: python` first attempt a versioned python executable for + the default language version before consulting a potentially unversioned + `sys.executable`. + - #3430 PR by @asottile. + +### Fixes +- Handle error during conflict detection when a file is named "HEAD" + - #3425 PR by @tusharsadhwani. + 4.1.0 - 2025-01-20 ================== diff --git a/setup.cfg b/setup.cfg index 60d976418..af34452df 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 4.1.0 +version = 4.2.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 6d47b8d52bd53320807886edd82b6fb4e1c67089 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 19:43:51 +0000 Subject: [PATCH 927/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v2.7.0 → v2.8.0](https://github.com/asottile/setup-cfg-fmt/compare/v2.7.0...v2.8.0) - [github.com/PyCQA/flake8: 7.1.2 → 7.2.0](https://github.com/PyCQA/flake8/compare/7.1.2...7.2.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b216fbd69..a19b44bc5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.7.0 + rev: v2.8.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports @@ -33,7 +33,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 - rev: 7.1.2 + rev: 7.2.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy From 43592c2a29c587aab7f70046a02ef95893841e67 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 19:44:12 +0000 Subject: [PATCH 928/967] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- setup.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index af34452df..90f49df9d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,7 +10,6 @@ author_email = asottile@umich.edu license = MIT license_files = LICENSE classifiers = - License :: OSI Approved :: MIT License Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: Implementation :: CPython From 466f6c4a3939375dc2dc7a2fc34f553c2715d738 Mon Sep 17 00:00:00 2001 From: Matthew Hughes Date: Sun, 13 Apr 2025 11:18:18 +0100 Subject: [PATCH 929/967] Fix permission errors for mounts in rootless docker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By running containers in a rootless docker context as root. This is because user and group IDs are remapped in the user namespaces uses by rootless docker, and it's unlikely that the current user ID will map to the same ID under this remap (see docs[1] for some more details). Specifically, it means ownership of mounted volumes will not be for the current user and trying to write can result in permission errors. This change borrows heavily from an existing PR[2]. The output format of `docker system info` I don't think is documented/guaranteed anywhere, but it should corresponding to the format of a `/info` API request to Docker[3] The added test _hopes_ to avoid regressions in this behaviour, but since tests aren't run in a rootless docker context on the PR checks (and I couldn't find an easy way to make it the case) there's still a risk of regressions sneaking in. Link: https://docs.docker.com/engine/security/rootless/ [1] Link: https://github.com/pre-commit/pre-commit/pull/1484/ [2] Link: https://docs.docker.com/reference/api/engine/version/v1.48/#tag/System/operation/SystemAuth [3] Co-authored-by: Kurt von Laven Co-authored-by: Fabrice Flore-Thébault --- pre_commit/languages/docker.py | 26 +++++++++++++++++++ tests/languages/docker_test.py | 47 ++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 4de1d5824..086e874d5 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -1,5 +1,6 @@ from __future__ import annotations +import functools import hashlib import json import os @@ -101,7 +102,32 @@ def install_environment( os.mkdir(directory) +@functools.lru_cache(maxsize=1) +def _is_rootless() -> bool: # pragma: win32 no cover + retcode, out, _ = cmd_output_b( + 'docker', 'system', 'info', '--format', '{{ json . }}', + ) + if retcode != 0: + return False + + info = json.loads(out) + try: + return ( + # docker: + # https://docs.docker.com/reference/api/engine/version/v1.48/#tag/System/operation/SystemInfo + 'name=rootless' in info.get('SecurityOptions', ()) or + # podman: + # https://docs.podman.io/en/latest/_static/api.html?version=v5.4#tag/system/operation/SystemInfoLibpod + info['host']['security']['rootless'] + ) + except KeyError: + return False + + def get_docker_user() -> tuple[str, ...]: # pragma: win32 no cover + if _is_rootless(): + return () + try: return ('-u', f'{os.getuid()}:{os.getgid()}') except AttributeError: diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 836382a8a..03235c46b 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -62,6 +62,42 @@ def invalid_attribute(): assert docker.get_docker_user() == () +@pytest.fixture(autouse=True) +def _avoid_cache(): + with mock.patch.object( + docker, + '_is_rootless', + docker._is_rootless.__wrapped__, + ): + yield + + +@pytest.mark.parametrize( + 'info_ret', + ( + (0, b'{"SecurityOptions": ["name=rootless","name=cgroupns"]}', b''), + (0, b'{"host": {"security": {"rootless": true}}}', b''), + ), +) +def test_docker_user_rootless(info_ret): + with mock.patch.object(docker, 'cmd_output_b', return_value=info_ret): + assert docker.get_docker_user() == () + + +@pytest.mark.parametrize( + 'info_ret', + ( + (0, b'{"SecurityOptions": ["name=cgroupns"]}', b''), + (0, b'{"host": {"security": {"rootless": false}}}', b''), + (0, b'{"respone_from_some_other_container_engine": true}', b''), + (1, b'', b''), + ), +) +def test_docker_user_non_rootless(info_ret): + with mock.patch.object(docker, 'cmd_output_b', return_value=info_ret): + assert docker.get_docker_user() != () + + def test_in_docker_no_file(): with mock.patch.object(builtins, 'open', side_effect=FileNotFoundError): assert docker._is_in_docker() is False @@ -195,3 +231,14 @@ def test_docker_hook(tmp_path): ret = run_language(tmp_path, docker, 'echo hello hello world') assert ret == (0, b'hello hello world\n') + + +@xfailif_windows # pragma: win32 no cover +def test_docker_hook_mount_permissions(tmp_path): + dockerfile = '''\ +FROM ubuntu:22.04 +''' + tmp_path.joinpath('Dockerfile').write_text(dockerfile) + + retcode, _ = run_language(tmp_path, docker, 'touch', ('README.md',)) + assert retcode == 0 From 43b426a501e621cc1c837f07b5633cb12525e79b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 26 May 2025 19:45:48 +0000 Subject: [PATCH 930/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder-python-imports: v3.14.0 → v3.15.0](https://github.com/asottile/reorder-python-imports/compare/v3.14.0...v3.15.0) - [github.com/asottile/add-trailing-comma: v3.1.0 → v3.2.0](https://github.com/asottile/add-trailing-comma/compare/v3.1.0...v3.2.0) - [github.com/asottile/pyupgrade: v3.19.1 → v3.20.0](https://github.com/asottile/pyupgrade/compare/v3.19.1...v3.20.0) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a19b44bc5..97d1174d0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,17 +14,17 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports - rev: v3.14.0 + rev: v3.15.0 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) args: [--py39-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma - rev: v3.1.0 + rev: v3.2.0 hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.19.1 + rev: v3.20.0 hooks: - id: pyupgrade args: [--py39-plus] From d4f0c6e8a7db7c29177f16fe3e56ab5c9aad7d73 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 19:57:10 +0000 Subject: [PATCH 931/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.15.0 → v1.16.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.15.0...v1.16.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 97d1174d0..4ddf34061 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.15.0 + rev: v1.16.0 hooks: - id: mypy additional_dependencies: [types-pyyaml] From d1d5b3d5648d213f8dcb1648eae77b411a27ac05 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 19:55:22 +0000 Subject: [PATCH 932/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 7.2.0 → 7.3.0](https://github.com/PyCQA/flake8/compare/7.2.0...7.3.0) - [github.com/pre-commit/mirrors-mypy: v1.16.0 → v1.16.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.16.0...v1.16.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4ddf34061..2dc7f4c11 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,11 +33,11 @@ repos: hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 - rev: 7.2.0 + rev: 7.3.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.16.0 + rev: v1.16.1 hooks: - id: mypy additional_dependencies: [types-pyyaml] From 4fd4537bc69e6804998d99e4851a9dbe43e91757 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 20:02:20 +0000 Subject: [PATCH 933/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.16.1 → v1.17.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.16.1...v1.17.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2dc7f4c11..3ef94b45c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.16.1 + rev: v1.17.0 hooks: - id: mypy additional_dependencies: [types-pyyaml] From 6f1f433a9cea94a70828ade95931a703c9a9c82b Mon Sep 17 00:00:00 2001 From: Eric Hanson <5846501+ericphanson@users.noreply.github.com> Date: Mon, 21 Jul 2025 18:05:54 +0200 Subject: [PATCH 934/967] Julia language: skip startup.jl file --- pre_commit/languages/julia.py | 5 +++-- tests/languages/julia_test.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/julia.py b/pre_commit/languages/julia.py index df91c0697..7559b5ba6 100644 --- a/pre_commit/languages/julia.py +++ b/pre_commit/languages/julia.py @@ -37,7 +37,7 @@ def run_hook( cmd = lang_base.hook_cmd(entry, args) script = cmd[0] if is_local else prefix.path(cmd[0]) - cmd = ('julia', script, *cmd[1:]) + cmd = ('julia', '--startup-file=no', script, *cmd[1:]) return lang_base.run_xargs( cmd, file_args, @@ -127,6 +127,7 @@ def install_environment( end """ cmd_output_b( - 'julia', '-e', julia_code, '--', envdir, *additional_dependencies, + 'julia', '--startup-file=no', '-e', julia_code, '--', envdir, + *additional_dependencies, cwd=prefix.prefix_dir, ) diff --git a/tests/languages/julia_test.py b/tests/languages/julia_test.py index 4ea3c25b2..175622d65 100644 --- a/tests/languages/julia_test.py +++ b/tests/languages/julia_test.py @@ -1,5 +1,8 @@ from __future__ import annotations +import os +from unittest import mock + from pre_commit.languages import julia from testing.language_helpers import run_language from testing.util import cwd @@ -28,6 +31,17 @@ def test_julia_hook(tmp_path): assert run_language(tmp_path, julia, 'src/main.jl') == expected +def test_julia_hook_with_startup(tmp_path): + depot_path = tmp_path.joinpath('depot') + depot_path.joinpath('config').mkdir(parents=True) + startup = depot_path.joinpath('config', 'startup.jl') + startup.write_text('error("Startup file used!")\n') + + depo_path_var = f'{depot_path}{os.pathsep}' + with mock.patch.dict(os.environ, {'JULIA_DEPOT_PATH': depo_path_var}): + test_julia_hook(tmp_path) + + def test_julia_hook_manifest(tmp_path): code = """ using Example From c8925a457afb1d6850c8f105671846bae408aae0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 20:31:31 +0000 Subject: [PATCH 935/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.17.0 → v1.17.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.17.0...v1.17.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3ef94b45c..da8e24ad9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.17.0 + rev: v1.17.1 hooks: - id: mypy additional_dependencies: [types-pyyaml] From f1cc7a445f1adbfc9ea4072e180fbe3054af669b Mon Sep 17 00:00:00 2001 From: Byoungchan Lee Date: Fri, 8 Aug 2025 17:02:53 +0900 Subject: [PATCH 936/967] Make Dart pre-commit hook compatible with the latest Dart SDKs Dart introduced sound null safety in version 2.12.0, and as of Dart 3, null safety is mandatory. Older Dart SDKs allowed both pre-null safety and null-safe packages, but modern Dart SDKs, where most source code is null-safe, now reject pre-null safety packages. The current `pubspec.yaml` template with `sdk: '>=2.10.0'` is incompatible with recent Dart SDKs, leading to the following error: An unexpected error has occurred: CalledProcessError: command: ('/path/to/dart-sdk/bin/dart', 'pub', 'get') return code: 65 stdout: Resolving dependencies... stderr: The lower bound of "sdk: '>=2.10.0'" must be 2.12.0' or higher to enable null safety. The current Dart SDK (3.8.3) only supports null safety. For details, see https://dart.dev/null-safety To ensure compatibility with the modern Dart ecosystem, this commit updates the minimum Dart SDK version to 2.12.0 or higher, which implicitly supports null safety. Additionally, `testing/get-dart.sh` has been updated to verify that the pre-commit hook functions correctly with the latest Dart versions. --- pre_commit/resources/empty_template_pubspec.yaml | 2 +- testing/get-dart.sh | 2 +- tests/languages/dart_test.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pre_commit/resources/empty_template_pubspec.yaml b/pre_commit/resources/empty_template_pubspec.yaml index 3be6ffe36..8306aeb64 100644 --- a/pre_commit/resources/empty_template_pubspec.yaml +++ b/pre_commit/resources/empty_template_pubspec.yaml @@ -1,4 +1,4 @@ name: pre_commit_empty_pubspec environment: - sdk: '>=2.10.0' + sdk: '>=2.12.0' executables: {} diff --git a/testing/get-dart.sh b/testing/get-dart.sh index 998b9d98f..3402c4212 100755 --- a/testing/get-dart.sh +++ b/testing/get-dart.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -VERSION=2.13.4 +VERSION=3.8.3 if [ "$OSTYPE" = msys ]; then URL="https://storage.googleapis.com/dart-archive/channels/stable/release/${VERSION}/sdk/dartsdk-windows-x64-release.zip" diff --git a/tests/languages/dart_test.py b/tests/languages/dart_test.py index 5bb5aa68f..213d888eb 100644 --- a/tests/languages/dart_test.py +++ b/tests/languages/dart_test.py @@ -10,7 +10,7 @@ def test_dart(tmp_path): pubspec_yaml = '''\ environment: - sdk: '>=2.10.0 <3.0.0' + sdk: '>=2.12.0 <4.0.0' name: hello_world_dart From 2a0bcea7570620416a550362d9b2d2b24eb80dd8 Mon Sep 17 00:00:00 2001 From: Byoungchan Lee Date: Fri, 8 Aug 2025 17:40:30 +0900 Subject: [PATCH 937/967] Downgrade Dart SDK version installed in the CI --- testing/get-dart.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/get-dart.sh b/testing/get-dart.sh index 3402c4212..b4545e71e 100755 --- a/testing/get-dart.sh +++ b/testing/get-dart.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -VERSION=3.8.3 +VERSION=2.19.6 if [ "$OSTYPE" = msys ]; then URL="https://storage.googleapis.com/dart-archive/channels/stable/release/${VERSION}/sdk/dartsdk-windows-x64-release.zip" From b74a22d96cca546b8e0bb9f68f1d7d8565205b65 Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Sat, 9 Aug 2025 14:54:49 -0400 Subject: [PATCH 938/967] v4.3.0 --- CHANGELOG.md | 13 +++++++++++++ setup.cfg | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b63f44317..42a63f781 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +4.3.0 - 2025-08-09 +================== + +### Features +- `language: docker` / `language: docker_image`: detect rootless docker. + - #3446 PR by @matthewhughes934. + - #1243 issue by @dkolepp. +- `language: julia`: avoid `startup.jl` when executing hooks. + - #3496 PR by @ericphanson. +- `language: dart`: support latest dart versions which require a higher sdk + lower bound. + - #3507 PR by @bc-lee. + 4.2.0 - 2025-03-18 ================== diff --git a/setup.cfg b/setup.cfg index 90f49df9d..9b0e02ad4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 4.2.0 +version = 4.3.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 87a681f8662554ee888a02e162d8772d64eee6cc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 20:46:13 +0000 Subject: [PATCH 939/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v5.0.0 → v6.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v5.0.0...v6.0.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index da8e24ad9..464cfe600 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer From c78f248c60846fa48c9d38b488f3acc0fed96207 Mon Sep 17 00:00:00 2001 From: JulianMaurin Date: Mon, 25 Aug 2025 23:20:07 +0200 Subject: [PATCH 940/967] Add fail-fast argument for run command --- pre_commit/commands/hook_impl.py | 1 + pre_commit/commands/run.py | 3 ++- pre_commit/main.py | 4 ++++ testing/util.py | 2 ++ tests/commands/run_test.py | 13 +++++++++++++ 5 files changed, 22 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index 49a80b7b3..de5c8f346 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -106,6 +106,7 @@ def _ns( hook=None, verbose=False, show_diff_on_failure=False, + fail_fast=False, ) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 793adbdb2..8ab505ffb 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -298,7 +298,8 @@ def _run_hooks( verbose=args.verbose, use_color=args.color, ) retval |= current_retval - if current_retval and (config['fail_fast'] or hook.fail_fast): + fail_fast = (config['fail_fast'] or hook.fail_fast or args.fail_fast) + if current_retval and fail_fast: break if retval and args.show_diff_on_failure and prior_diff: if args.all_files: diff --git a/pre_commit/main.py b/pre_commit/main.py index 559c927c9..fc4531b82 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -76,6 +76,10 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: '--show-diff-on-failure', action='store_true', help='When hooks fail, run `git diff` directly afterward.', ) + parser.add_argument( + '--fail-fast', action='store_true', + help='Stop after the first failing hook.', + ) parser.add_argument( '--hook-stage', choices=clientlib.STAGES, diff --git a/testing/util.py b/testing/util.py index 08d52cbc3..1646ccd2a 100644 --- a/testing/util.py +++ b/testing/util.py @@ -40,6 +40,7 @@ def run_opts( color=False, verbose=False, hook=None, + fail_fast=False, remote_branch='', local_branch='', from_ref='', @@ -65,6 +66,7 @@ def run_opts( color=color, verbose=verbose, hook=hook, + fail_fast=fail_fast, remote_branch=remote_branch, local_branch=local_branch, from_ref=from_ref, diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 50a20f377..e4af1e162 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -1104,6 +1104,19 @@ def test_fail_fast_not_prev_failures(cap_out, store, repo_with_failing_hook): assert printed.count(b'run me!') == 1 +def test_fail_fast_run_arg(cap_out, store, repo_with_failing_hook): + with modify_config() as config: + # More than one hook to demonstrate early exit + config['repos'][0]['hooks'] *= 2 + stage_a_file() + + ret, printed = _do_run( + cap_out, store, repo_with_failing_hook, run_opts(fail_fast=True), + ) + # it should have only run one hook due to the CLI flag + assert printed.count(b'Failing hook') == 1 + + def test_classifier_removes_dne(): classifier = Classifier(('this_file_does_not_exist',)) assert classifier.filenames == [] From e67183040220cd8662b5b886b24841e2d04bac9c Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Sat, 6 Sep 2025 14:20:01 -0400 Subject: [PATCH 941/967] store_true does not need default=... --- pre_commit/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 559c927c9..b7ac3dc26 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -62,10 +62,10 @@ def _add_hook_type_option(parser: argparse.ArgumentParser) -> None: def _add_run_options(parser: argparse.ArgumentParser) -> None: parser.add_argument('hook', nargs='?', help='A single hook-id to run') - parser.add_argument('--verbose', '-v', action='store_true', default=False) + parser.add_argument('--verbose', '-v', action='store_true') mutex_group = parser.add_mutually_exclusive_group(required=False) mutex_group.add_argument( - '--all-files', '-a', action='store_true', default=False, + '--all-files', '-a', action='store_true', help='Run on all the files in the repo.', ) mutex_group.add_argument( @@ -275,7 +275,7 @@ def _add_cmd(name: str, *, help: str) -> argparse.ArgumentParser: ) _add_hook_type_option(install_parser) install_parser.add_argument( - '--allow-missing-config', action='store_true', default=False, + '--allow-missing-config', action='store_true', help=( 'Whether to allow a missing `pre-commit` configuration file ' 'or exit with a failure code.' From 2930ea0fcd481f4c2cbeae0245a8bb748bae905a Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Sat, 6 Sep 2025 14:40:20 -0400 Subject: [PATCH 942/967] handle `SecurityOptions: null` in docker response --- pre_commit/languages/docker.py | 2 +- tests/languages/docker_test.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 086e874d5..d5ce1eb70 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -115,7 +115,7 @@ def _is_rootless() -> bool: # pragma: win32 no cover return ( # docker: # https://docs.docker.com/reference/api/engine/version/v1.48/#tag/System/operation/SystemInfo - 'name=rootless' in info.get('SecurityOptions', ()) or + 'name=rootless' in (info.get('SecurityOptions') or ()) or # podman: # https://docs.podman.io/en/latest/_static/api.html?version=v5.4#tag/system/operation/SystemInfoLibpod info['host']['security']['rootless'] diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 03235c46b..b830439a2 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -89,7 +89,8 @@ def test_docker_user_rootless(info_ret): ( (0, b'{"SecurityOptions": ["name=cgroupns"]}', b''), (0, b'{"host": {"security": {"rootless": false}}}', b''), - (0, b'{"respone_from_some_other_container_engine": true}', b''), + (0, b'{"response_from_some_other_container_engine": true}', b''), + (0, b'{"SecurityOptions": null}', b''), (1, b'', b''), ), ) From ad0d4cd4271cab68ddbf5e5c3386f38320e0ccd2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 20:44:04 +0000 Subject: [PATCH 943/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.17.1 → v1.18.2](https://github.com/pre-commit/mirrors-mypy/compare/v1.17.1...v1.18.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 464cfe600..0a24427f9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.17.1 + rev: v1.18.2 hooks: - id: mypy additional_dependencies: [types-pyyaml] From f415f6c4d72224363ba794429b25cc3f52e04933 Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Thu, 9 Oct 2025 17:44:05 -0400 Subject: [PATCH 944/967] py310+ Committed via https://github.com/asottile/all-repos --- .github/workflows/languages.yaml | 4 ++-- .github/workflows/main.yml | 4 ++-- .pre-commit-config.yaml | 4 ++-- pre_commit/commands/migrate_config.py | 2 +- pre_commit/file_lock.py | 2 +- pre_commit/languages/golang.py | 3 +-- pre_commit/store.py | 2 +- pre_commit/util.py | 2 +- pre_commit/xargs.py | 2 +- setup.cfg | 2 +- 10 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.github/workflows/languages.yaml b/.github/workflows/languages.yaml index fccf29892..be8963bac 100644 --- a/.github/workflows/languages.yaml +++ b/.github/workflows/languages.yaml @@ -21,7 +21,7 @@ jobs: fetch-depth: 0 - uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: '3.10' - name: install deps run: python -mpip install -e . -r requirements-dev.txt - name: vars @@ -39,7 +39,7 @@ jobs: - uses: asottile/workflows/.github/actions/fast-checkout@v1.8.1 - uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: '3.10' - run: echo "$CONDA\Scripts" >> "$GITHUB_PATH" shell: bash diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7fda646ff..02b11ae28 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,10 +14,10 @@ jobs: main-windows: uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1 with: - env: '["py39"]' + env: '["py310"]' os: windows-latest main-linux: uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1 with: - env: '["py39", "py310", "py311", "py312"]' + env: '["py310", "py311", "py312", "py313"]' os: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0a24427f9..58b96f76f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) - args: [--py39-plus, --add-import, 'from __future__ import annotations'] + args: [--py310-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma rev: v3.2.0 hooks: @@ -27,7 +27,7 @@ repos: rev: v3.20.0 hooks: - id: pyupgrade - args: [--py39-plus] + args: [--py310-plus] - repo: https://github.com/hhatto/autopep8 rev: v2.3.2 hooks: diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index c5d47a08e..b04c53a5e 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -3,7 +3,7 @@ import functools import itertools import textwrap -from typing import Callable +from collections.abc import Callable import cfgv import yaml diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index c840ad8b5..6223f869e 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -3,8 +3,8 @@ import contextlib import errno import sys +from collections.abc import Callable from collections.abc import Generator -from typing import Callable if sys.platform == 'win32': # pragma: no cover (windows) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 678c04b14..bedbd114b 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -90,8 +90,7 @@ def _infer_go_version(version: str) -> str: if version != C.DEFAULT: return version resp = urllib.request.urlopen('https://go.dev/dl/?mode=json') - # TODO: 3.9+ .removeprefix('go') - return json.load(resp)[0]['version'][2:] + return json.load(resp)[0]['version'].removeprefix('go') def _get_url(version: str) -> str: diff --git a/pre_commit/store.py b/pre_commit/store.py index 1235942c5..9e3b40485 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -5,9 +5,9 @@ import os.path import sqlite3 import tempfile +from collections.abc import Callable from collections.abc import Generator from collections.abc import Sequence -from typing import Callable import pre_commit.constants as C from pre_commit import clientlib diff --git a/pre_commit/util.py b/pre_commit/util.py index e199d0807..19b1880bf 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -8,10 +8,10 @@ import stat import subprocess import sys +from collections.abc import Callable from collections.abc import Generator from types import TracebackType from typing import Any -from typing import Callable from pre_commit import parse_shebang diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index a1345b583..7c98d1674 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -7,12 +7,12 @@ import os import subprocess import sys +from collections.abc import Callable from collections.abc import Generator from collections.abc import Iterable from collections.abc import MutableMapping from collections.abc import Sequence from typing import Any -from typing import Callable from typing import TypeVar from pre_commit import parse_shebang diff --git a/setup.cfg b/setup.cfg index 9b0e02ad4..8fb6e6aab 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,7 +23,7 @@ install_requires = nodeenv>=0.11.1 pyyaml>=5.1 virtualenv>=20.10.0 -python_requires = >=3.9 +python_requires = >=3.10 [options.packages.find] exclude = From 221637b0cbdfbfe8ca209ba5df0111b08f9d8cda Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 20:38:45 +0000 Subject: [PATCH 945/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v2.8.0 → v3.1.0](https://github.com/asottile/setup-cfg-fmt/compare/v2.8.0...v3.1.0) - [github.com/asottile/reorder-python-imports: v3.15.0 → v3.16.0](https://github.com/asottile/reorder-python-imports/compare/v3.15.0...v3.16.0) - [github.com/asottile/add-trailing-comma: v3.2.0 → v4.0.0](https://github.com/asottile/add-trailing-comma/compare/v3.2.0...v4.0.0) - [github.com/asottile/pyupgrade: v3.20.0 → v3.21.0](https://github.com/asottile/pyupgrade/compare/v3.20.0...v3.21.0) --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 58b96f76f..b1623a640 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,21 +10,21 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.8.0 + rev: v3.1.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports - rev: v3.15.0 + rev: v3.16.0 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) args: [--py310-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma - rev: v3.2.0 + rev: v4.0.0 hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.20.0 + rev: v3.21.0 hooks: - id: pyupgrade args: [--py310-plus] From ddfcf4034bc72445497b5e6708205523a9da7ed7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 16 Oct 2025 10:23:27 -0400 Subject: [PATCH 946/967] fix deprecated call --- pre_commit/git.py | 2 +- setup.cfg | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 2f424f89e..ec1928f37 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -219,7 +219,7 @@ def check_for_cygwin_mismatch() -> None: if is_cygwin_python ^ is_cygwin_git: exe_type = {True: '(cygwin)', False: '(windows)'} - logger.warn( + logger.warning( f'pre-commit has detected a mix of cygwin python / git\n' f'This combination is not supported, it is likely you will ' f'receive an error later in the program.\n' diff --git a/setup.cfg b/setup.cfg index 8fb6e6aab..17c3fe0eb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -52,6 +52,7 @@ check_untyped_defs = true disallow_any_generics = true disallow_incomplete_defs = true disallow_untyped_defs = true +enable_error_code = deprecated warn_redundant_casts = true warn_unused_ignores = true From fc33a62f3c55c671cdef8306b6c3dc91d81e2b4a Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Fri, 24 Oct 2025 15:18:07 -0400 Subject: [PATCH 947/967] upgrade rbenv / ruby-build --- pre_commit/resources/rbenv.tar.gz | Bin 32545 -> 31297 bytes pre_commit/resources/ruby-build.tar.gz | Bin 88488 -> 93998 bytes testing/make-archives | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/resources/rbenv.tar.gz b/pre_commit/resources/rbenv.tar.gz index 111546e3dd9796511942c278495c21fe1ef947ae..b5df08744c17c6fd950d89e8a5caf6186905dedc 100644 GIT binary patch literal 31297 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 B1~UKv diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz index a4f7eb24fd0a7e4d59cf120014d8b054555b8b0f..5c82c90609384836116acd7df8bbe9f79a4c7a86 100644 GIT binary patch literal 93998 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 literal 88488 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 Date: Sat, 8 Nov 2025 13:16:33 -0500 Subject: [PATCH 948/967] remove redundant system spaces test `test_args_with_spaces_and_quotes` also covers this behaviour --- .../system_hook_with_spaces_repo/.pre-commit-hooks.yaml | 5 ----- tests/repository_test.py | 7 ------- 2 files changed, 12 deletions(-) delete mode 100644 testing/resources/system_hook_with_spaces_repo/.pre-commit-hooks.yaml diff --git a/testing/resources/system_hook_with_spaces_repo/.pre-commit-hooks.yaml b/testing/resources/system_hook_with_spaces_repo/.pre-commit-hooks.yaml deleted file mode 100644 index b2c347c14..000000000 --- a/testing/resources/system_hook_with_spaces_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: system-hook-with-spaces - name: System hook with spaces - entry: bash -c 'echo "Hello World"' - language: system - files: \.sh$ diff --git a/tests/repository_test.py b/tests/repository_test.py index b54c910d3..f1559301f 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -80,13 +80,6 @@ def _test_hook_repo( assert out == expected -def test_system_hook_with_spaces(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'system_hook_with_spaces_repo', - 'system-hook-with-spaces', [os.devnull], b'Hello World\n', - ) - - def test_missing_executable(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'not_found_exe', From 95eec7500464500d2ca0cc13d0986000508830e5 Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Sat, 8 Nov 2025 13:33:50 -0500 Subject: [PATCH 949/967] rm python3_hooks_repo --- .pre-commit-config.yaml | 2 +- .../resources/python3_hooks_repo/.pre-commit-hooks.yaml | 6 ------ testing/resources/python3_hooks_repo/py3_hook.py | 8 -------- testing/resources/python3_hooks_repo/setup.py | 8 -------- 4 files changed, 1 insertion(+), 23 deletions(-) delete mode 100644 testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/python3_hooks_repo/py3_hook.py delete mode 100644 testing/resources/python3_hooks_repo/setup.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b1623a640..fa0773656 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: rev: v3.16.0 hooks: - id: reorder-python-imports - exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) + exclude: ^pre_commit/resources/ args: [--py310-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma rev: v4.0.0 diff --git a/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 2c2370092..000000000 --- a/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,6 +0,0 @@ -- id: python3-hook - name: Python 3 Hook - entry: python3-hook - language: python - language_version: python3 - files: \.py$ diff --git a/testing/resources/python3_hooks_repo/py3_hook.py b/testing/resources/python3_hooks_repo/py3_hook.py deleted file mode 100644 index 8c9cda4c6..000000000 --- a/testing/resources/python3_hooks_repo/py3_hook.py +++ /dev/null @@ -1,8 +0,0 @@ -import sys - - -def main(): - print(sys.version_info[0]) - print(repr(sys.argv[1:])) - print('Hello World') - return 0 diff --git a/testing/resources/python3_hooks_repo/setup.py b/testing/resources/python3_hooks_repo/setup.py deleted file mode 100644 index 9125dc1df..000000000 --- a/testing/resources/python3_hooks_repo/setup.py +++ /dev/null @@ -1,8 +0,0 @@ -from setuptools import setup - -setup( - name='python3_hook', - version='0.0.0', - py_modules=['py3_hook'], - entry_points={'console_scripts': ['python3-hook = py3_hook:main']}, -) From aa2961c122b4aa834c77e612232c154f9439c388 Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Sat, 8 Nov 2025 14:31:11 -0500 Subject: [PATCH 950/967] fix missing context in error for stages --- pre_commit/clientlib.py | 9 +++++---- tests/clientlib_test.py | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index c0f736d92..51514bd36 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -116,11 +116,12 @@ def check(self, dct: dict[str, Any]) -> None: if self.key not in dct: return - val = dct[self.key] - cfgv.check_array(cfgv.check_any)(val) + with cfgv.validate_context(f'At key: {self.key}'): + val = dct[self.key] + cfgv.check_array(cfgv.check_any)(val) - val = [transform_stage(v) for v in val] - cfgv.check_array(cfgv.check_one_of(STAGES))(val) + val = [transform_stage(v) for v in val] + cfgv.check_array(cfgv.check_one_of(STAGES))(val) def apply_default(self, dct: dict[str, Any]) -> None: if self.key not in dct: diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 7aa84af0e..2251abc4e 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -309,6 +309,27 @@ def test_validate_optional_sensible_regex_at_top_level(caplog, regex, warning): assert caplog.record_tuples == [('pre_commit', logging.WARNING, warning)] +def test_invalid_stages_error(): + cfg = {'repos': [sample_local_config()]} + cfg['repos'][0]['hooks'][0]['stages'] = ['invalid'] + + with pytest.raises(cfgv.ValidationError) as excinfo: + cfgv.validate(cfg, CONFIG_SCHEMA) + + assert str(excinfo.value) == ( + '\n' + '==> At Config()\n' + '==> At key: repos\n' + "==> At Repository(repo='local')\n" + '==> At key: hooks\n' + "==> At Hook(id='do_not_commit')\n" + # this line was missing due to the custom validator + '==> At key: stages\n' + '==> At index 0\n' + "=====> Expected one of commit-msg, manual, post-checkout, post-commit, post-merge, post-rewrite, pre-commit, pre-merge-commit, pre-push, pre-rebase, prepare-commit-msg but got: 'invalid'" # noqa: E501 + ) + + def test_warning_for_deprecated_stages(caplog): config_obj = sample_local_config() config_obj['hooks'][0]['stages'] = ['commit', 'push'] From 725acc969a28a6bc9a7e2260f035426bc932e8da Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Sat, 8 Nov 2025 13:13:18 -0500 Subject: [PATCH 951/967] rename system and script languages to unsupported / unsupported_script --- pre_commit/all_languages.py | 8 ++-- pre_commit/clientlib.py | 47 +++++++++++++++++-- .../languages/{system.py => unsupported.py} | 0 .../{script.py => unsupported_script.py} | 0 tests/clientlib_test.py | 20 ++++++++ tests/languages/system_test.py | 9 ---- ...ipt_test.py => unsupported_script_test.py} | 6 +-- tests/languages/unsupported_test.py | 10 ++++ tests/repository_test.py | 14 +++--- 9 files changed, 88 insertions(+), 26 deletions(-) rename pre_commit/languages/{system.py => unsupported.py} (100%) rename pre_commit/languages/{script.py => unsupported_script.py} (100%) delete mode 100644 tests/languages/system_test.py rename tests/languages/{script_test.py => unsupported_script_test.py} (63%) create mode 100644 tests/languages/unsupported_test.py diff --git a/pre_commit/all_languages.py b/pre_commit/all_languages.py index ba569c377..166bc167f 100644 --- a/pre_commit/all_languages.py +++ b/pre_commit/all_languages.py @@ -19,9 +19,9 @@ from pre_commit.languages import r from pre_commit.languages import ruby from pre_commit.languages import rust -from pre_commit.languages import script from pre_commit.languages import swift -from pre_commit.languages import system +from pre_commit.languages import unsupported +from pre_commit.languages import unsupported_script languages: dict[str, Language] = { @@ -43,8 +43,8 @@ 'r': r, 'ruby': ruby, 'rust': rust, - 'script': script, 'swift': swift, - 'system': system, + 'unsupported': unsupported, + 'unsupported_script': unsupported_script, } language_names = sorted(languages) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 51514bd36..eb0fd4d68 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -6,6 +6,7 @@ import re import shlex import sys +from collections.abc import Callable from collections.abc import Sequence from typing import Any from typing import NamedTuple @@ -190,6 +191,42 @@ def remove_default(self, dct: dict[str, Any]) -> None: raise NotImplementedError +def _translate_language(name: str) -> str: + return { + 'system': 'unsupported', + 'script': 'unsupported_script', + }.get(name, name) + + +class LanguageMigration(NamedTuple): # remove + key: str + check_fn: Callable[[object], None] + + def check(self, dct: dict[str, Any]) -> None: + if self.key not in dct: + return + + with cfgv.validate_context(f'At key: {self.key}'): + self.check_fn(_translate_language(dct[self.key])) + + def apply_default(self, dct: dict[str, Any]) -> None: + if self.key not in dct: + return + + dct[self.key] = _translate_language(dct[self.key]) + + def remove_default(self, dct: dict[str, Any]) -> None: + raise NotImplementedError + + +class LanguageMigrationRequired(LanguageMigration): # replace with Required + def check(self, dct: dict[str, Any]) -> None: + if self.key not in dct: + raise cfgv.ValidationError(f'Missing required key: {self.key}') + + super().check(dct) + + MANIFEST_HOOK_DICT = cfgv.Map( 'Hook', 'id', @@ -203,7 +240,7 @@ def remove_default(self, dct: dict[str, Any]) -> None: cfgv.Required('id', cfgv.check_string), cfgv.Required('name', cfgv.check_string), cfgv.Required('entry', cfgv.check_string), - cfgv.Required('language', cfgv.check_one_of(language_names)), + LanguageMigrationRequired('language', cfgv.check_one_of(language_names)), cfgv.Optional('alias', cfgv.check_string, ''), cfgv.Optional('files', check_string_regex, ''), @@ -368,8 +405,10 @@ def check(self, dct: dict[str, Any]) -> None: 'Hook', 'id', cfgv.Required('id', cfgv.check_string), cfgv.Required('id', cfgv.check_one_of(tuple(k for k, _ in _meta))), - # language must be system - cfgv.Optional('language', cfgv.check_one_of({'system'}), 'system'), + # language must be `unsupported` + cfgv.Optional( + 'language', cfgv.check_one_of({'unsupported'}), 'unsupported', + ), # entry cannot be overridden NotAllowed('entry', cfgv.check_any), *( @@ -402,8 +441,10 @@ def check(self, dct: dict[str, Any]) -> None: for item in MANIFEST_HOOK_DICT.items if item.key != 'id' if item.key != 'stages' + if item.key != 'language' # remove ), StagesMigrationNoDefault('stages', []), + LanguageMigration('language', cfgv.check_one_of(language_names)), # remove *_COMMON_HOOK_WARNINGS, ) LOCAL_HOOK_DICT = cfgv.Map( diff --git a/pre_commit/languages/system.py b/pre_commit/languages/unsupported.py similarity index 100% rename from pre_commit/languages/system.py rename to pre_commit/languages/unsupported.py diff --git a/pre_commit/languages/script.py b/pre_commit/languages/unsupported_script.py similarity index 100% rename from pre_commit/languages/script.py rename to pre_commit/languages/unsupported_script.py diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 2251abc4e..93c698f79 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -380,6 +380,26 @@ def test_no_warning_for_non_deprecated_default_stages(caplog): assert caplog.record_tuples == [] +def test_unsupported_language_migration(): + cfg = {'repos': [sample_local_config(), sample_local_config()]} + cfg['repos'][0]['hooks'][0]['language'] = 'system' + cfg['repos'][1]['hooks'][0]['language'] = 'script' + + cfgv.validate(cfg, CONFIG_SCHEMA) + ret = cfgv.apply_defaults(cfg, CONFIG_SCHEMA) + + assert ret['repos'][0]['hooks'][0]['language'] == 'unsupported' + assert ret['repos'][1]['hooks'][0]['language'] == 'unsupported_script' + + +def test_unsupported_language_migration_language_required(): + cfg = {'repos': [sample_local_config()]} + del cfg['repos'][0]['hooks'][0]['language'] + + with pytest.raises(cfgv.ValidationError): + cfgv.validate(cfg, CONFIG_SCHEMA) + + @pytest.mark.parametrize( 'manifest_obj', ( diff --git a/tests/languages/system_test.py b/tests/languages/system_test.py deleted file mode 100644 index dcd9cf1e0..000000000 --- a/tests/languages/system_test.py +++ /dev/null @@ -1,9 +0,0 @@ -from __future__ import annotations - -from pre_commit.languages import system -from testing.language_helpers import run_language - - -def test_system_language(tmp_path): - expected = (0, b'hello hello world\n') - assert run_language(tmp_path, system, 'echo hello hello world') == expected diff --git a/tests/languages/script_test.py b/tests/languages/unsupported_script_test.py similarity index 63% rename from tests/languages/script_test.py rename to tests/languages/unsupported_script_test.py index a02f615a9..b15b67e76 100644 --- a/tests/languages/script_test.py +++ b/tests/languages/unsupported_script_test.py @@ -1,14 +1,14 @@ from __future__ import annotations -from pre_commit.languages import script +from pre_commit.languages import unsupported_script from pre_commit.util import make_executable from testing.language_helpers import run_language -def test_script_language(tmp_path): +def test_unsupported_script_language(tmp_path): exe = tmp_path.joinpath('main') exe.write_text('#!/usr/bin/env bash\necho hello hello world\n') make_executable(exe) expected = (0, b'hello hello world\n') - assert run_language(tmp_path, script, 'main') == expected + assert run_language(tmp_path, unsupported_script, 'main') == expected diff --git a/tests/languages/unsupported_test.py b/tests/languages/unsupported_test.py new file mode 100644 index 000000000..7f8461e02 --- /dev/null +++ b/tests/languages/unsupported_test.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from pre_commit.languages import unsupported +from testing.language_helpers import run_language + + +def test_unsupported_language(tmp_path): + expected = (0, b'hello hello world\n') + ret = run_language(tmp_path, unsupported, 'echo hello hello world') + assert ret == expected diff --git a/tests/repository_test.py b/tests/repository_test.py index f1559301f..b1c7a0024 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -17,7 +17,7 @@ from pre_commit.clientlib import load_manifest from pre_commit.hook import Hook from pre_commit.languages import python -from pre_commit.languages import system +from pre_commit.languages import unsupported from pre_commit.prefix import Prefix from pre_commit.repository import _hook_installed from pre_commit.repository import all_hooks @@ -424,7 +424,7 @@ def test_manifest_hooks(tempdir_factory, store): exclude_types=[], files='', id='bash_hook', - language='script', + language='unsupported_script', language_version='default', log_file='', minimum_pre_commit_version='0', @@ -457,7 +457,7 @@ def test_non_installable_hook_error_for_language_version(store, caplog): 'hooks': [{ 'id': 'system-hook', 'name': 'system-hook', - 'language': 'system', + 'language': 'unsupported', 'entry': 'python3 -c "import sys; print(sys.version)"', 'language_version': 'python3.10', }], @@ -469,7 +469,7 @@ def test_non_installable_hook_error_for_language_version(store, caplog): msg, = caplog.messages assert msg == ( 'The hook `system-hook` specifies `language_version` but is using ' - 'language `system` which does not install an environment. ' + 'language `unsupported` which does not install an environment. ' 'Perhaps you meant to use a specific language?' ) @@ -480,7 +480,7 @@ def test_non_installable_hook_error_for_additional_dependencies(store, caplog): 'hooks': [{ 'id': 'system-hook', 'name': 'system-hook', - 'language': 'system', + 'language': 'unsupported', 'entry': 'python3 -c "import sys; print(sys.version)"', 'additional_dependencies': ['astpretty'], }], @@ -492,14 +492,14 @@ def test_non_installable_hook_error_for_additional_dependencies(store, caplog): msg, = caplog.messages assert msg == ( 'The hook `system-hook` specifies `additional_dependencies` but is ' - 'using language `system` which does not install an environment. ' + 'using language `unsupported` which does not install an environment. ' 'Perhaps you meant to use a specific language?' ) def test_args_with_spaces_and_quotes(tmp_path): ret = run_language( - tmp_path, system, + tmp_path, unsupported, f"{shlex.quote(sys.executable)} -c 'import sys; print(sys.argv[1:])'", ('i have spaces', 'and"\'quotes', '$and !this'), ) From f80801d75a429d5eafa1d87e9f88f73b108d1890 Mon Sep 17 00:00:00 2001 From: Radek Hrbacek Date: Fri, 5 Sep 2025 15:01:10 +0200 Subject: [PATCH 952/967] Fix docker-in-docker detection for cgroups v2 --- pre_commit/languages/docker.py | 40 +++--- tests/languages/docker_test.py | 230 +++++++++++++++++++++++++-------- 2 files changed, 201 insertions(+), 69 deletions(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index d5ce1eb70..7f45ac865 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -1,9 +1,11 @@ from __future__ import annotations +import contextlib import functools import hashlib import json import os +import re from collections.abc import Sequence from pre_commit import lang_base @@ -17,31 +19,33 @@ health_check = lang_base.basic_health_check in_env = lang_base.no_env # no special environment for docker +_HOSTNAME_MOUNT_RE = re.compile( + rb""" + /containers + (?:/overlay-containers)? + /([a-z0-9]{64}) + (?:/userdata)? + /hostname + """, + re.VERBOSE, +) -def _is_in_docker() -> bool: - try: - with open('/proc/1/cgroup', 'rb') as f: - return b'docker' in f.read() - except FileNotFoundError: - return False +def _get_container_id() -> str | None: + with contextlib.suppress(FileNotFoundError): + with open('/proc/1/mountinfo', 'rb') as f: + for line in f: + m = _HOSTNAME_MOUNT_RE.search(line) + if m: + return m[1].decode() -def _get_container_id() -> str: - # It's assumed that we already check /proc/1/cgroup in _is_in_docker. The - # cpuset cgroup controller existed since cgroups were introduced so this - # way of getting the container ID is pretty reliable. - with open('/proc/1/cgroup', 'rb') as f: - for line in f.readlines(): - if line.split(b':')[1] == b'cpuset': - return os.path.basename(line.split(b':')[2]).strip().decode() - raise RuntimeError('Failed to find the container ID in /proc/1/cgroup.') + return None def _get_docker_path(path: str) -> str: - if not _is_in_docker(): - return path - container_id = _get_container_id() + if container_id is None: + return path try: _, out, _ = cmd_output_b('docker', 'inspect', container_id) diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index b830439a2..e269976f7 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -14,40 +14,173 @@ from testing.language_helpers import run_language from testing.util import xfailif_windows -DOCKER_CGROUP_EXAMPLE = b'''\ -12:hugetlb:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -11:blkio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -10:freezer:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -9:cpu,cpuacct:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -8:pids:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -7:rdma:/ -6:net_cls,net_prio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -5:cpuset:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -4:devices:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -3:memory:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -2:perf_event:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -1:name=systemd:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -0::/system.slice/containerd.service +DOCKER_CGROUPS_V1_MOUNTINFO_EXAMPLE = b'''\ +759 717 0:52 / / rw,relatime master:300 - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/PCPE5P5IVGM7CFCPJR353N3ONK:/var/lib/docker/overlay2/l/EQFSDHFAJ333VEMEJD4ZTRIZCB,upperdir=/var/lib/docker/overlay2/0d9f6bf186030d796505b87d6daa92297355e47641e283d3c09d83a7f221e462/diff,workdir=/var/lib/docker/overlay2/0d9f6bf186030d796505b87d6daa92297355e47641e283d3c09d83a7f221e462/work +760 759 0:58 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +761 759 0:59 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64 +762 761 0:60 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +763 759 0:61 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro +764 763 0:62 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755,inode64 +765 764 0:29 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/systemd ro,nosuid,nodev,noexec,relatime master:11 - cgroup cgroup rw,xattr,name=systemd +766 764 0:32 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/rdma ro,nosuid,nodev,noexec,relatime master:15 - cgroup cgroup rw,rdma +767 764 0:33 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/cpu,cpuacct ro,nosuid,nodev,noexec,relatime master:16 - cgroup cgroup rw,cpu,cpuacct +768 764 0:34 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/cpuset ro,nosuid,nodev,noexec,relatime master:17 - cgroup cgroup rw,cpuset +769 764 0:35 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/pids ro,nosuid,nodev,noexec,relatime master:18 - cgroup cgroup rw,pids +770 764 0:36 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/memory ro,nosuid,nodev,noexec,relatime master:19 - cgroup cgroup rw,memory +771 764 0:37 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/perf_event ro,nosuid,nodev,noexec,relatime master:20 - cgroup cgroup rw,perf_event +772 764 0:38 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/net_cls,net_prio ro,nosuid,nodev,noexec,relatime master:21 - cgroup cgroup rw,net_cls,net_prio +773 764 0:39 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/blkio ro,nosuid,nodev,noexec,relatime master:22 - cgroup cgroup rw,blkio +774 764 0:40 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/misc ro,nosuid,nodev,noexec,relatime master:23 - cgroup cgroup rw,misc +775 764 0:41 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/hugetlb ro,nosuid,nodev,noexec,relatime master:24 - cgroup cgroup rw,hugetlb +776 764 0:42 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/devices ro,nosuid,nodev,noexec,relatime master:25 - cgroup cgroup rw,devices +777 764 0:43 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/freezer ro,nosuid,nodev,noexec,relatime master:26 - cgroup cgroup rw,freezer +778 761 0:57 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw +779 761 0:63 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k,inode64 +780 759 8:5 /var/lib/docker/containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/sda5 rw,errors=remount-ro +781 759 8:5 /var/lib/docker/containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/hostname /etc/hostname rw,relatime - ext4 /dev/sda5 rw,errors=remount-ro +782 759 8:5 /var/lib/docker/containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/hosts /etc/hosts rw,relatime - ext4 /dev/sda5 rw,errors=remount-ro +718 761 0:60 /0 /dev/console rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +719 760 0:58 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw +720 760 0:58 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw +721 760 0:58 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw +722 760 0:58 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw +723 760 0:58 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw +724 760 0:64 / /proc/asound ro,relatime - tmpfs tmpfs ro,inode64 +725 760 0:65 / /proc/acpi ro,relatime - tmpfs tmpfs ro,inode64 +726 760 0:59 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64 +727 760 0:59 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64 +728 760 0:59 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64 +729 760 0:66 / /proc/scsi ro,relatime - tmpfs tmpfs ro,inode64 +730 763 0:67 / /sys/firmware ro,relatime - tmpfs tmpfs ro,inode64 +731 763 0:68 / /sys/devices/virtual/powercap ro,relatime - tmpfs tmpfs ro,inode64 +''' # noqa: E501 + +DOCKER_CGROUPS_V2_MOUNTINFO_EXAMPLE = b'''\ +721 386 0:45 / / rw,relatime master:218 - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/QHZ7OM7P4AQD3XLG274ZPWAJCV:/var/lib/docker/overlay2/l/5RFG6SZWVGOG2NKEYXJDQCQYX5,upperdir=/var/lib/docker/overlay2/e4ad859fc5d4791932b9b976052f01fb0063e01de3cef916e40ae2121f6a166e/diff,workdir=/var/lib/docker/overlay2/e4ad859fc5d4791932b9b976052f01fb0063e01de3cef916e40ae2121f6a166e/work,nouserxattr +722 721 0:48 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +723 721 0:50 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64 +724 723 0:51 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +725 721 0:52 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro +726 725 0:26 / /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - cgroup2 cgroup rw,nsdelegate,memory_recursiveprot +727 723 0:47 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw +728 723 0:53 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k,inode64 +729 721 8:3 /var/lib/docker/containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/sda3 rw,errors=remount-ro +730 721 8:3 /var/lib/docker/containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/hostname /etc/hostname rw,relatime - ext4 /dev/sda3 rw,errors=remount-ro +731 721 8:3 /var/lib/docker/containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/hosts /etc/hosts rw,relatime - ext4 /dev/sda3 rw,errors=remount-ro +387 723 0:51 /0 /dev/console rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +388 722 0:48 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw +389 722 0:48 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw +525 722 0:48 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw +526 722 0:48 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw +571 722 0:48 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw +572 722 0:57 / /proc/asound ro,relatime - tmpfs tmpfs ro,inode64 +575 722 0:58 / /proc/acpi ro,relatime - tmpfs tmpfs ro,inode64 +576 722 0:50 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64 +577 722 0:50 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64 +578 722 0:50 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64 +579 722 0:59 / /proc/scsi ro,relatime - tmpfs tmpfs ro,inode64 +580 725 0:60 / /sys/firmware ro,relatime - tmpfs tmpfs ro,inode64 +''' # noqa: E501 + +PODMAN_CGROUPS_V1_MOUNTINFO_EXAMPLE = b'''\ +1200 915 0:57 / / rw,relatime - overlay overlay rw,lowerdir=/home/asottile/.local/share/containers/storage/overlay/l/ZWAU3VY3ZHABQJRBUAFPBX7R5D,upperdir=/home/asottile/.local/share/containers/storage/overlay/72504ef163fda63838930450553b7306412ccad139a007626732b3dc43af5200/diff,workdir=/home/asottile/.local/share/containers/storage/overlay/72504ef163fda63838930450553b7306412ccad139a007626732b3dc43af5200/work,volatile,userxattr +1204 1200 0:62 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +1205 1200 0:63 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,uid=1000,gid=1000,inode64 +1206 1200 0:64 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs rw +1207 1205 0:65 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=100004,mode=620,ptmxmode=666 +1208 1205 0:61 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw +1209 1200 0:53 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/.containerenv /run/.containerenv rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=814036k,mode=700,uid=1000,gid=1000,inode64 +1210 1200 0:53 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/resolv.conf /etc/resolv.conf rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=814036k,mode=700,uid=1000,gid=1000,inode64 +1211 1200 0:53 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/hosts /etc/hosts rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=814036k,mode=700,uid=1000,gid=1000,inode64 +1212 1205 0:56 / /dev/shm rw,relatime - tmpfs shm rw,size=64000k,uid=1000,gid=1000,inode64 +1213 1200 0:53 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/hostname /etc/hostname rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=814036k,mode=700,uid=1000,gid=1000,inode64 +1214 1206 0:66 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs cgroup rw,size=1024k,uid=1000,gid=1000,inode64 +1215 1214 0:43 / /sys/fs/cgroup/freezer ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,freezer +1216 1214 0:42 /user.slice /sys/fs/cgroup/devices ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,devices +1217 1214 0:41 / /sys/fs/cgroup/hugetlb ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,hugetlb +1218 1214 0:40 / /sys/fs/cgroup/misc ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,misc +1219 1214 0:39 / /sys/fs/cgroup/blkio ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,blkio +1220 1214 0:38 / /sys/fs/cgroup/net_cls,net_prio ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,net_cls,net_prio +1221 1214 0:37 / /sys/fs/cgroup/perf_event ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,perf_event +1222 1214 0:36 /user.slice/user-1000.slice/user@1000.service /sys/fs/cgroup/memory ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,memory +1223 1214 0:35 /user.slice/user-1000.slice/user@1000.service /sys/fs/cgroup/pids ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,pids +1224 1214 0:34 / /sys/fs/cgroup/cpuset ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuset +1225 1214 0:33 / /sys/fs/cgroup/cpu,cpuacct ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpu,cpuacct +1226 1214 0:32 / /sys/fs/cgroup/rdma ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,rdma +1227 1214 0:29 /user.slice/user-1000.slice/user@1000.service/apps.slice/apps-org.gnome.Terminal.slice/vte-spawn-0c50448e-b395-4d76-8b92-379f16e5066f.scope /sys/fs/cgroup/systemd ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,xattr,name=systemd +1228 1205 0:5 /null /dev/null rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64 +1229 1205 0:5 /zero /dev/zero rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64 +1230 1205 0:5 /full /dev/full rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64 +1231 1205 0:5 /tty /dev/tty rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64 +1232 1205 0:5 /random /dev/random rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64 +1233 1205 0:5 /urandom /dev/urandom rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64 +1234 1204 0:67 / /proc/acpi ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64 +1235 1204 0:5 /null /proc/kcore rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64 +1236 1204 0:5 /null /proc/keys rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64 +1237 1204 0:5 /null /proc/timer_list rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64 +1238 1204 0:68 / /proc/scsi ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64 +1239 1206 0:69 / /sys/firmware ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64 +1240 1206 0:70 / /sys/dev/block ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64 +1241 1204 0:62 /asound /proc/asound ro,relatime - proc proc rw +1242 1204 0:62 /bus /proc/bus ro,relatime - proc proc rw +1243 1204 0:62 /fs /proc/fs ro,relatime - proc proc rw +1244 1204 0:62 /irq /proc/irq ro,relatime - proc proc rw +1245 1204 0:62 /sys /proc/sys ro,relatime - proc proc rw +1256 1204 0:62 /sysrq-trigger /proc/sysrq-trigger ro,relatime - proc proc rw +916 1205 0:65 /0 /dev/console rw,relatime - devpts devpts rw,gid=100004,mode=620,ptmxmode=666 +''' # noqa: E501 + +PODMAN_CGROUPS_V2_MOUNTINFO_EXAMPLE = b'''\ +685 690 0:63 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/resolv.conf /etc/resolv.conf rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=1637624k,nr_inodes=409406,mode=700,uid=1000,gid=1000,inode64 +686 690 0:63 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/hosts /etc/hosts rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=1637624k,nr_inodes=409406,mode=700,uid=1000,gid=1000,inode64 +687 692 0:50 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=64000k,uid=1000,gid=1000,inode64 +688 690 0:63 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/.containerenv /run/.containerenv rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=1637624k,nr_inodes=409406,mode=700,uid=1000,gid=1000,inode64 +689 690 0:63 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/hostname /etc/hostname rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=1637624k,nr_inodes=409406,mode=700,uid=1000,gid=1000,inode64 +690 546 0:55 / / rw,relatime - overlay overlay rw,lowerdir=/home/asottile/.local/share/containers/storage/overlay/l/NPOHYOD3PI3YW6TQSGBOVOUSK6,upperdir=/home/asottile/.local/share/containers/storage/overlay/565c206fb79f876ffd5f069b8bd7a97fb5e47d5d07396b0c395a4ed6725d4a8e/diff,workdir=/home/asottile/.local/share/containers/storage/overlay/565c206fb79f876ffd5f069b8bd7a97fb5e47d5d07396b0c395a4ed6725d4a8e/work,redirect_dir=nofollow,uuid=on,volatile,userxattr +691 690 0:59 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +692 690 0:61 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,uid=1000,gid=1000,inode64 +693 690 0:62 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs rw +694 692 0:66 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=100004,mode=620,ptmxmode=666 +695 692 0:58 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw +696 693 0:28 / /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - cgroup2 cgroup2 rw,nsdelegate,memory_recursiveprot +698 692 0:6 /null /dev/null rw,nosuid,noexec,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +699 692 0:6 /zero /dev/zero rw,nosuid,noexec,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +700 692 0:6 /full /dev/full rw,nosuid,noexec,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +701 692 0:6 /tty /dev/tty rw,nosuid,noexec,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +702 692 0:6 /random /dev/random rw,nosuid,noexec,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +703 692 0:6 /urandom /dev/urandom rw,nosuid,noexec,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +704 691 0:67 / /proc/acpi ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64 +705 691 0:6 /null /proc/kcore ro,nosuid,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +706 691 0:6 /null /proc/keys ro,nosuid,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +707 691 0:6 /null /proc/latency_stats ro,nosuid,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +708 691 0:6 /null /proc/timer_list ro,nosuid,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +709 691 0:68 / /proc/scsi ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64 +710 693 0:69 / /sys/firmware ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64 +711 693 0:70 / /sys/dev/block ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64 +712 693 0:71 / /sys/devices/virtual/powercap ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64 +713 691 0:59 /asound /proc/asound ro,nosuid,nodev,noexec,relatime - proc proc rw +714 691 0:59 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw +715 691 0:59 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw +716 691 0:59 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw +717 691 0:59 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw +718 691 0:59 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw +547 692 0:66 /0 /dev/console rw,relatime - devpts devpts rw,gid=100004,mode=620,ptmxmode=666 ''' # noqa: E501 # The ID should match the above cgroup example. CONTAINER_ID = 'c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7' # noqa: E501 -NON_DOCKER_CGROUP_EXAMPLE = b'''\ -12:perf_event:/ -11:hugetlb:/ -10:devices:/ -9:blkio:/ -8:rdma:/ -7:cpuset:/ -6:cpu,cpuacct:/ -5:freezer:/ -4:memory:/ -3:pids:/ -2:net_cls,net_prio:/ -1:name=systemd:/init.scope -0::/init.scope -''' +NON_DOCKER_MOUNTINFO_EXAMPLE = b'''\ +21 27 0:19 / /sys rw,nosuid,nodev,noexec,relatime shared:7 - sysfs sysfs rw +22 27 0:20 / /proc rw,nosuid,nodev,noexec,relatime shared:14 - proc proc rw +23 27 0:5 / /dev rw,nosuid,relatime shared:2 - devtmpfs udev rw,size=10219484k,nr_inodes=2554871,mode=755,inode64 +24 23 0:21 / /dev/pts rw,nosuid,noexec,relatime shared:3 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +25 27 0:22 / /run rw,nosuid,nodev,noexec,relatime shared:5 - tmpfs tmpfs rw,size=2047768k,mode=755,inode64 +27 1 8:2 / / rw,relatime shared:1 - ext4 /dev/sda2 rw,errors=remount-ro +28 21 0:6 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime shared:8 - securityfs securityfs rw +29 23 0:24 / /dev/shm rw,nosuid,nodev shared:4 - tmpfs tmpfs rw,inode64 +30 25 0:25 / /run/lock rw,nosuid,nodev,noexec,relatime shared:6 - tmpfs tmpfs rw,size=5120k,inode64 +''' # noqa: E501 def test_docker_fallback_user(): @@ -99,9 +232,9 @@ def test_docker_user_non_rootless(info_ret): assert docker.get_docker_user() != () -def test_in_docker_no_file(): +def test_container_id_no_file(): with mock.patch.object(builtins, 'open', side_effect=FileNotFoundError): - assert docker._is_in_docker() is False + assert docker._get_container_id() is None def _mock_open(data): @@ -113,38 +246,33 @@ def _mock_open(data): ) -def test_in_docker_docker_in_file(): - with _mock_open(DOCKER_CGROUP_EXAMPLE): - assert docker._is_in_docker() is True - - -def test_in_docker_docker_not_in_file(): - with _mock_open(NON_DOCKER_CGROUP_EXAMPLE): - assert docker._is_in_docker() is False +def test_container_id_not_in_file(): + with _mock_open(NON_DOCKER_MOUNTINFO_EXAMPLE): + assert docker._get_container_id() is None def test_get_container_id(): - with _mock_open(DOCKER_CGROUP_EXAMPLE): + with _mock_open(DOCKER_CGROUPS_V1_MOUNTINFO_EXAMPLE): + assert docker._get_container_id() == CONTAINER_ID + with _mock_open(DOCKER_CGROUPS_V2_MOUNTINFO_EXAMPLE): + assert docker._get_container_id() == CONTAINER_ID + with _mock_open(PODMAN_CGROUPS_V1_MOUNTINFO_EXAMPLE): + assert docker._get_container_id() == CONTAINER_ID + with _mock_open(PODMAN_CGROUPS_V2_MOUNTINFO_EXAMPLE): assert docker._get_container_id() == CONTAINER_ID - - -def test_get_container_id_failure(): - with _mock_open(b''), pytest.raises(RuntimeError): - docker._get_container_id() def test_get_docker_path_not_in_docker_returns_same(): - with mock.patch.object(docker, '_is_in_docker', return_value=False): + with _mock_open(b''): assert docker._get_docker_path('abc') == 'abc' @pytest.fixture def in_docker(): - with mock.patch.object(docker, '_is_in_docker', return_value=True): - with mock.patch.object( - docker, '_get_container_id', return_value=CONTAINER_ID, - ): - yield + with mock.patch.object( + docker, '_get_container_id', return_value=CONTAINER_ID, + ): + yield def _linux_commonpath(): From 17cf8864737af2ce75c73839a0cdedc26ce50598 Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Sat, 8 Nov 2025 16:11:43 -0500 Subject: [PATCH 953/967] v4.4.0 --- CHANGELOG.md | 28 ++++++++++++++++++++++++++-- setup.cfg | 2 +- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42a63f781..b27af5e7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,27 @@ +4.4.0 - 2025-11-08 +================== + +### Features +- Add `--fail-fast` option to `pre-commit run`. + - #3528 PR by @JulianMaurin. +- Upgrade `ruby-build` / `rbenv`. + - #3566 PR by @asottile. + - #3565 issue by @MRigal. +- Add `language: unsupported` / `language: unsupported_script` as aliases + for `language: system` / `language: script` (which will eventually be + deprecated). + - #3577 PR by @asottile. +- Add support docker-in-docker detection for cgroups v2. + - #3535 PR by @br-rhrbacek. + - #3360 issue by @JasonAlt. + +### Fixes +- Handle when docker gives `SecurityOptions: null`. + - #3537 PR by @asottile. + - #3514 issue by @jenstroeger. +- Fix error context for invalid `stages` in `.pre-commit-config.yaml`. + - #3576 PR by @asottile. + 4.3.0 - 2025-08-09 ================== @@ -71,7 +95,7 @@ - #3315 PR by @asottile. - #2732 issue by @asottile. -### Migrating +### Updating - `language: python_venv` has been removed -- use `language: python` instead. - #3320 PR by @asottile. - #2734 issue by @asottile. @@ -159,7 +183,7 @@ - Use `time.monotonic()` for more accurate hook timing. - #3024 PR by @adamchainz. -### Migrating +### Updating - Require npm 6.x+ for `language: node` hooks. - #2996 PR by @RoelAdriaans. - #1983 issue by @henryiii. diff --git a/setup.cfg b/setup.cfg index 17c3fe0eb..be031c3ef 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 4.3.0 +version = 4.4.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From d5c273a2ba0c712659640e9487adb38bd7da68f6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 9 Nov 2025 16:57:40 -0500 Subject: [PATCH 954/967] refactor gc into store this will make refactoring this easier later and limits the api surface of Store --- pre_commit/commands/gc.py | 82 +-------------------------------- pre_commit/store.py | 96 +++++++++++++++++++++++++++++++-------- tests/commands/gc_test.py | 9 ++-- tests/store_test.py | 25 +++++++--- 4 files changed, 101 insertions(+), 111 deletions(-) diff --git a/pre_commit/commands/gc.py b/pre_commit/commands/gc.py index 6892e097c..d1941e4bb 100644 --- a/pre_commit/commands/gc.py +++ b/pre_commit/commands/gc.py @@ -1,89 +1,9 @@ from __future__ import annotations -import os.path -from typing import Any - -import pre_commit.constants as C from pre_commit import output -from pre_commit.clientlib import InvalidConfigError -from pre_commit.clientlib import InvalidManifestError -from pre_commit.clientlib import load_config -from pre_commit.clientlib import load_manifest -from pre_commit.clientlib import LOCAL -from pre_commit.clientlib import META from pre_commit.store import Store -def _mark_used_repos( - store: Store, - all_repos: dict[tuple[str, str], str], - unused_repos: set[tuple[str, str]], - repo: dict[str, Any], -) -> None: - if repo['repo'] == META: - return - elif repo['repo'] == LOCAL: - for hook in repo['hooks']: - deps = hook.get('additional_dependencies') - unused_repos.discard(( - store.db_repo_name(repo['repo'], deps), C.LOCAL_REPO_VERSION, - )) - else: - key = (repo['repo'], repo['rev']) - path = all_repos.get(key) - # can't inspect manifest if it isn't cloned - if path is None: - return - - try: - manifest = load_manifest(os.path.join(path, C.MANIFEST_FILE)) - except InvalidManifestError: - return - else: - unused_repos.discard(key) - by_id = {hook['id']: hook for hook in manifest} - - for hook in repo['hooks']: - if hook['id'] not in by_id: - continue - - deps = hook.get( - 'additional_dependencies', - by_id[hook['id']]['additional_dependencies'], - ) - unused_repos.discard(( - store.db_repo_name(repo['repo'], deps), repo['rev'], - )) - - -def _gc_repos(store: Store) -> int: - configs = store.select_all_configs() - repos = store.select_all_repos() - - # delete config paths which do not exist - dead_configs = [p for p in configs if not os.path.exists(p)] - live_configs = [p for p in configs if os.path.exists(p)] - - all_repos = {(repo, ref): path for repo, ref, path in repos} - unused_repos = set(all_repos) - for config_path in live_configs: - try: - config = load_config(config_path) - except InvalidConfigError: - dead_configs.append(config_path) - continue - else: - for repo in config['repos']: - _mark_used_repos(store, all_repos, unused_repos, repo) - - store.delete_configs(dead_configs) - for db_repo_name, ref in unused_repos: - store.delete_repo(db_repo_name, ref, all_repos[(db_repo_name, ref)]) - return len(unused_repos) - - def gc(store: Store) -> int: - with store.exclusive_lock(): - repos_removed = _gc_repos(store) - output.write_line(f'{repos_removed} repo(s) removed.') + output.write_line(f'{store.gc()} repo(s) removed.') return 0 diff --git a/pre_commit/store.py b/pre_commit/store.py index 9e3b40485..34c5f0d92 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -8,6 +8,7 @@ from collections.abc import Callable from collections.abc import Generator from collections.abc import Sequence +from typing import Any import pre_commit.constants as C from pre_commit import clientlib @@ -96,7 +97,7 @@ def __init__(self, directory: str | None = None) -> None: ' PRIMARY KEY (repo, ref)' ');', ) - self._create_config_table(db) + self._create_configs_table(db) # Atomic file move os.replace(tmpfile, self.db_path) @@ -215,7 +216,7 @@ def make_local(self, deps: Sequence[str]) -> str: 'local', C.LOCAL_REPO_VERSION, deps, _make_local_repo, ) - def _create_config_table(self, db: sqlite3.Connection) -> None: + def _create_configs_table(self, db: sqlite3.Connection) -> None: db.executescript( 'CREATE TABLE IF NOT EXISTS configs (' ' path TEXT NOT NULL,' @@ -232,28 +233,83 @@ def mark_config_used(self, path: str) -> None: return with self.connect() as db: # TODO: eventually remove this and only create in _create - self._create_config_table(db) + self._create_configs_table(db) db.execute('INSERT OR IGNORE INTO configs VALUES (?)', (path,)) - def select_all_configs(self) -> list[str]: - with self.connect() as db: - self._create_config_table(db) - rows = db.execute('SELECT path FROM configs').fetchall() - return [path for path, in rows] + def _mark_used_repos( + self, + all_repos: dict[tuple[str, str], str], + unused_repos: set[tuple[str, str]], + repo: dict[str, Any], + ) -> None: + if repo['repo'] == clientlib.META: + return + elif repo['repo'] == clientlib.LOCAL: + for hook in repo['hooks']: + deps = hook.get('additional_dependencies') + unused_repos.discard(( + self.db_repo_name(repo['repo'], deps), + C.LOCAL_REPO_VERSION, + )) + else: + key = (repo['repo'], repo['rev']) + path = all_repos.get(key) + # can't inspect manifest if it isn't cloned + if path is None: + return - def delete_configs(self, configs: list[str]) -> None: - with self.connect() as db: - rows = [(path,) for path in configs] - db.executemany('DELETE FROM configs WHERE path = ?', rows) + try: + manifest = clientlib.load_manifest( + os.path.join(path, C.MANIFEST_FILE), + ) + except clientlib.InvalidManifestError: + return + else: + unused_repos.discard(key) + by_id = {hook['id']: hook for hook in manifest} - def select_all_repos(self) -> list[tuple[str, str, str]]: - with self.connect() as db: - return db.execute('SELECT repo, ref, path from repos').fetchall() + for hook in repo['hooks']: + if hook['id'] not in by_id: + continue - def delete_repo(self, db_repo_name: str, ref: str, path: str) -> None: - with self.connect() as db: - db.execute( + deps = hook.get( + 'additional_dependencies', + by_id[hook['id']]['additional_dependencies'], + ) + unused_repos.discard(( + self.db_repo_name(repo['repo'], deps), repo['rev'], + )) + + def gc(self) -> int: + with self.exclusive_lock(), self.connect() as db: + self._create_configs_table(db) + + repos = db.execute('SELECT repo, ref, path FROM repos').fetchall() + all_repos = {(repo, ref): path for repo, ref, path in repos} + unused_repos = set(all_repos) + + configs_rows = db.execute('SELECT path FROM configs').fetchall() + configs = [path for path, in configs_rows] + + dead_configs = [] + for config_path in configs: + try: + config = clientlib.load_config(config_path) + except clientlib.InvalidConfigError: + dead_configs.append(config_path) + continue + else: + for repo in config['repos']: + self._mark_used_repos(all_repos, unused_repos, repo) + + paths = [(path,) for path in dead_configs] + db.executemany('DELETE FROM configs WHERE path = ?', paths) + + db.executemany( 'DELETE FROM repos WHERE repo = ? and ref = ?', - (db_repo_name, ref), + sorted(unused_repos), ) - rmtree(path) + for k in unused_repos: + rmtree(all_repos[k]) + + return len(unused_repos) diff --git a/tests/commands/gc_test.py b/tests/commands/gc_test.py index 95113ed5c..85e66977e 100644 --- a/tests/commands/gc_test.py +++ b/tests/commands/gc_test.py @@ -19,11 +19,13 @@ def _repo_count(store): - return len(store.select_all_repos()) + with store.connect() as db: + return db.execute('SELECT COUNT(1) FROM repos').fetchone()[0] def _config_count(store): - return len(store.select_all_configs()) + with store.connect() as db: + return db.execute('SELECT COUNT(1) FROM configs').fetchone()[0] def _remove_config_assert_cleared(store, cap_out): @@ -153,7 +155,8 @@ def test_invalid_manifest_gcd(tempdir_factory, store, in_git_dir, cap_out): install_hooks(C.CONFIG_FILE, store) # we'll "break" the manifest to simulate an old version clone - (_, _, path), = store.select_all_repos() + with store.connect() as db: + path, = db.execute('SELECT path FROM repos').fetchone() os.remove(os.path.join(path, C.MANIFEST_FILE)) assert _config_count(store) == 1 diff --git a/tests/store_test.py b/tests/store_test.py index 7d4dffb09..4b04a8e7f 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -22,6 +22,17 @@ from testing.util import xfailif_windows +def _select_all_configs(store: Store) -> list[str]: + with store.connect() as db: + rows = db.execute('SELECT * FROM configs').fetchall() + return [path for path, in rows] + + +def _select_all_repos(store: Store) -> list[tuple[str, str, str]]: + with store.connect() as db: + return db.execute('SELECT repo, ref, path FROM repos').fetchall() + + def test_our_session_fixture_works(): """There's a session fixture which makes `Store` invariantly raise to prevent writing to the home directory. @@ -91,7 +102,7 @@ def test_clone(store, tempdir_factory, caplog): assert git.head_rev(ret) == rev # Assert there's an entry in the sqlite db for this - assert store.select_all_repos() == [(path, rev, ret)] + assert _select_all_repos(store) == [(path, rev, ret)] def test_warning_for_deprecated_stages_on_init(store, tempdir_factory, caplog): @@ -217,7 +228,7 @@ def fake_shallow_clone(self, *args, **kwargs): assert git.head_rev(ret) == rev # Assert there's an entry in the sqlite db for this - assert store.select_all_repos() == [(path, rev, ret)] + assert _select_all_repos(store) == [(path, rev, ret)] def test_clone_tag_not_on_mainline(store, tempdir_factory): @@ -265,7 +276,7 @@ def test_mark_config_as_used(store, tmpdir): with tmpdir.as_cwd(): f = tmpdir.join('f').ensure() store.mark_config_used('f') - assert store.select_all_configs() == [f.strpath] + assert _select_all_configs(store) == [f.strpath] def test_mark_config_as_used_idempotent(store, tmpdir): @@ -275,7 +286,7 @@ def test_mark_config_as_used_idempotent(store, tmpdir): def test_mark_config_as_used_does_not_exist(store): store.mark_config_used('f') - assert store.select_all_configs() == [] + assert _select_all_configs(store) == [] def _simulate_pre_1_14_0(store): @@ -283,9 +294,9 @@ def _simulate_pre_1_14_0(store): db.executescript('DROP TABLE configs') -def test_select_all_configs_roll_forward(store): +def test_gc_roll_forward(store): _simulate_pre_1_14_0(store) - assert store.select_all_configs() == [] + assert store.gc() == 0 def test_mark_config_as_used_roll_forward(store, tmpdir): @@ -314,7 +325,7 @@ def _chmod_minus_w(p): assert store.readonly # should be skipped due to readonly store.mark_config_used(str(cfg)) - assert store.select_all_configs() == [] + assert _select_all_configs(store) == [] def test_clone_with_recursive_submodules(store, tmp_path): From 063229aee77ba2da3e9ed5c8217070b4128234fd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 20:59:54 +0000 Subject: [PATCH 955/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.21.0 → v3.21.1](https://github.com/asottile/pyupgrade/compare/v3.21.0...v3.21.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fa0773656..e47d56ca3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.21.0 + rev: v3.21.1 hooks: - id: pyupgrade args: [--py310-plus] From 66278a9a0b69a69fde820d2b85a7e198eae52981 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Nov 2025 14:29:50 -0500 Subject: [PATCH 956/967] move logic for gc back to commands.gc --- pre_commit/commands/gc.py | 91 ++++++++++++++++++++++++++++++++++++++- pre_commit/store.py | 80 ---------------------------------- tests/commands/gc_test.py | 8 ++++ tests/store_test.py | 13 +----- 4 files changed, 100 insertions(+), 92 deletions(-) diff --git a/pre_commit/commands/gc.py b/pre_commit/commands/gc.py index d1941e4bb..975d5e4c1 100644 --- a/pre_commit/commands/gc.py +++ b/pre_commit/commands/gc.py @@ -1,9 +1,98 @@ from __future__ import annotations +import os.path +from typing import Any + +import pre_commit.constants as C from pre_commit import output +from pre_commit.clientlib import InvalidConfigError +from pre_commit.clientlib import InvalidManifestError +from pre_commit.clientlib import load_config +from pre_commit.clientlib import load_manifest +from pre_commit.clientlib import LOCAL +from pre_commit.clientlib import META from pre_commit.store import Store +from pre_commit.util import rmtree + + +def _mark_used_repos( + store: Store, + all_repos: dict[tuple[str, str], str], + unused_repos: set[tuple[str, str]], + repo: dict[str, Any], +) -> None: + if repo['repo'] == META: + return + elif repo['repo'] == LOCAL: + for hook in repo['hooks']: + deps = hook.get('additional_dependencies') + unused_repos.discard(( + store.db_repo_name(repo['repo'], deps), + C.LOCAL_REPO_VERSION, + )) + else: + key = (repo['repo'], repo['rev']) + path = all_repos.get(key) + # can't inspect manifest if it isn't cloned + if path is None: + return + + try: + manifest = load_manifest(os.path.join(path, C.MANIFEST_FILE)) + except InvalidManifestError: + return + else: + unused_repos.discard(key) + by_id = {hook['id']: hook for hook in manifest} + + for hook in repo['hooks']: + if hook['id'] not in by_id: + continue + + deps = hook.get( + 'additional_dependencies', + by_id[hook['id']]['additional_dependencies'], + ) + unused_repos.discard(( + store.db_repo_name(repo['repo'], deps), repo['rev'], + )) + + +def _gc(store: Store) -> int: + with store.exclusive_lock(), store.connect() as db: + store._create_configs_table(db) + + repos = db.execute('SELECT repo, ref, path FROM repos').fetchall() + all_repos = {(repo, ref): path for repo, ref, path in repos} + unused_repos = set(all_repos) + + configs_rows = db.execute('SELECT path FROM configs').fetchall() + configs = [path for path, in configs_rows] + + dead_configs = [] + for config_path in configs: + try: + config = load_config(config_path) + except InvalidConfigError: + dead_configs.append(config_path) + continue + else: + for repo in config['repos']: + _mark_used_repos(store, all_repos, unused_repos, repo) + + paths = [(path,) for path in dead_configs] + db.executemany('DELETE FROM configs WHERE path = ?', paths) + + db.executemany( + 'DELETE FROM repos WHERE repo = ? and ref = ?', + sorted(unused_repos), + ) + for k in unused_repos: + rmtree(all_repos[k]) + + return len(unused_repos) def gc(store: Store) -> int: - output.write_line(f'{store.gc()} repo(s) removed.') + output.write_line(f'{_gc(store)} repo(s) removed.') return 0 diff --git a/pre_commit/store.py b/pre_commit/store.py index 34c5f0d92..dc90c0519 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -8,7 +8,6 @@ from collections.abc import Callable from collections.abc import Generator from collections.abc import Sequence -from typing import Any import pre_commit.constants as C from pre_commit import clientlib @@ -18,7 +17,6 @@ from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b from pre_commit.util import resource_text -from pre_commit.util import rmtree logger = logging.getLogger('pre_commit') @@ -235,81 +233,3 @@ def mark_config_used(self, path: str) -> None: # TODO: eventually remove this and only create in _create self._create_configs_table(db) db.execute('INSERT OR IGNORE INTO configs VALUES (?)', (path,)) - - def _mark_used_repos( - self, - all_repos: dict[tuple[str, str], str], - unused_repos: set[tuple[str, str]], - repo: dict[str, Any], - ) -> None: - if repo['repo'] == clientlib.META: - return - elif repo['repo'] == clientlib.LOCAL: - for hook in repo['hooks']: - deps = hook.get('additional_dependencies') - unused_repos.discard(( - self.db_repo_name(repo['repo'], deps), - C.LOCAL_REPO_VERSION, - )) - else: - key = (repo['repo'], repo['rev']) - path = all_repos.get(key) - # can't inspect manifest if it isn't cloned - if path is None: - return - - try: - manifest = clientlib.load_manifest( - os.path.join(path, C.MANIFEST_FILE), - ) - except clientlib.InvalidManifestError: - return - else: - unused_repos.discard(key) - by_id = {hook['id']: hook for hook in manifest} - - for hook in repo['hooks']: - if hook['id'] not in by_id: - continue - - deps = hook.get( - 'additional_dependencies', - by_id[hook['id']]['additional_dependencies'], - ) - unused_repos.discard(( - self.db_repo_name(repo['repo'], deps), repo['rev'], - )) - - def gc(self) -> int: - with self.exclusive_lock(), self.connect() as db: - self._create_configs_table(db) - - repos = db.execute('SELECT repo, ref, path FROM repos').fetchall() - all_repos = {(repo, ref): path for repo, ref, path in repos} - unused_repos = set(all_repos) - - configs_rows = db.execute('SELECT path FROM configs').fetchall() - configs = [path for path, in configs_rows] - - dead_configs = [] - for config_path in configs: - try: - config = clientlib.load_config(config_path) - except clientlib.InvalidConfigError: - dead_configs.append(config_path) - continue - else: - for repo in config['repos']: - self._mark_used_repos(all_repos, unused_repos, repo) - - paths = [(path,) for path in dead_configs] - db.executemany('DELETE FROM configs WHERE path = ?', paths) - - db.executemany( - 'DELETE FROM repos WHERE repo = ? and ref = ?', - sorted(unused_repos), - ) - for k in unused_repos: - rmtree(all_repos[k]) - - return len(unused_repos) diff --git a/tests/commands/gc_test.py b/tests/commands/gc_test.py index 85e66977e..992b02f3b 100644 --- a/tests/commands/gc_test.py +++ b/tests/commands/gc_test.py @@ -165,3 +165,11 @@ def test_invalid_manifest_gcd(tempdir_factory, store, in_git_dir, cap_out): assert _config_count(store) == 1 assert _repo_count(store) == 0 assert cap_out.get().splitlines()[-1] == '1 repo(s) removed.' + + +def test_gc_pre_1_14_roll_forward(store, cap_out): + with store.connect() as db: # simulate pre-1.14.0 + db.executescript('DROP TABLE configs') + + assert not gc(store) + assert cap_out.get() == '0 repo(s) removed.\n' diff --git a/tests/store_test.py b/tests/store_test.py index 4b04a8e7f..13f198ea2 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -289,18 +289,9 @@ def test_mark_config_as_used_does_not_exist(store): assert _select_all_configs(store) == [] -def _simulate_pre_1_14_0(store): - with store.connect() as db: - db.executescript('DROP TABLE configs') - - -def test_gc_roll_forward(store): - _simulate_pre_1_14_0(store) - assert store.gc() == 0 - - def test_mark_config_as_used_roll_forward(store, tmpdir): - _simulate_pre_1_14_0(store) + with store.connect() as db: # simulate pre-1.14.0 + db.executescript('DROP TABLE configs') test_mark_config_as_used(store, tmpdir) From 844dacc168d68a32553ecf8a99178ab395fdb11e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Nov 2025 14:57:01 -0500 Subject: [PATCH 957/967] add forward-compat error message --- pre_commit/clientlib.py | 11 ++++++++++- tests/clientlib_test.py | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index eb0fd4d68..51f14d26e 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -270,10 +270,19 @@ class InvalidManifestError(FatalError): pass +def _load_manifest_forward_compat(contents: str) -> object: + obj = yaml_load(contents) + if isinstance(obj, dict): + check_min_version('5') + raise AssertionError('unreachable') + else: + return obj + + load_manifest = functools.partial( cfgv.load_from_filename, schema=MANIFEST_SCHEMA, - load_strategy=yaml_load, + load_strategy=_load_manifest_forward_compat, exc_tp=InvalidManifestError, ) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 93c698f79..2c42b80cf 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -12,6 +12,8 @@ from pre_commit.clientlib import CONFIG_REPO_DICT from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import DEFAULT_LANGUAGE_VERSION +from pre_commit.clientlib import InvalidManifestError +from pre_commit.clientlib import load_manifest from pre_commit.clientlib import MANIFEST_HOOK_DICT from pre_commit.clientlib import MANIFEST_SCHEMA from pre_commit.clientlib import META_HOOK_DICT @@ -588,3 +590,18 @@ def test_config_hook_stages_defaulting(): 'id': 'fake-hook', 'stages': ['commit-msg', 'pre-push', 'pre-commit', 'pre-merge-commit'], } + + +def test_manifest_v5_forward_compat(tmp_path): + manifest = tmp_path.joinpath('.pre-commit-hooks.yaml') + manifest.write_text('hooks: {}') + + with pytest.raises(InvalidManifestError) as excinfo: + load_manifest(manifest) + assert str(excinfo.value) == ( + f'\n' + f'==> File {manifest}\n' + f'=====> \n' + f'=====> pre-commit version 5 is required but version {C.VERSION} ' + f'is installed. Perhaps run `pip install --upgrade pre-commit`.' + ) From 8d34f95308fc4c14dea3d3e90153acfdaf55e2de Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Fri, 21 Nov 2025 15:09:41 -0500 Subject: [PATCH 958/967] use ExitStack instead of start + stop --- tests/main_test.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/main_test.py b/tests/main_test.py index 945349fa4..325792d89 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -1,6 +1,7 @@ from __future__ import annotations import argparse +import contextlib import os.path from unittest import mock @@ -97,11 +98,9 @@ def test_adjust_args_try_repo_repo_relative(in_git_dir): @pytest.fixture def mock_commands(): - mcks = {fn: mock.patch.object(main, fn).start() for fn in FNS} - ret = auto_namedtuple(**mcks) - yield ret - for mck in ret: - mck.stop() + with contextlib.ExitStack() as ctx: + mcks = {f: ctx.enter_context(mock.patch.object(main, f)) for f in FNS} + yield auto_namedtuple(**mcks) @pytest.fixture From bdf68790b78158268bbc8482f76491a61d75809a Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Fri, 21 Nov 2025 16:38:55 -0500 Subject: [PATCH 959/967] add pre-commit hazmat --- pre_commit/commands/hazmat.py | 95 +++++++++++++++++++++++++++++++++ pre_commit/lang_base.py | 6 ++- pre_commit/main.py | 10 +++- tests/commands/hazmat_test.py | 99 +++++++++++++++++++++++++++++++++++ tests/lang_base_test.py | 12 +++++ tests/main_test.py | 12 +++++ tests/repository_test.py | 11 ++++ 7 files changed, 243 insertions(+), 2 deletions(-) create mode 100644 pre_commit/commands/hazmat.py create mode 100644 tests/commands/hazmat_test.py diff --git a/pre_commit/commands/hazmat.py b/pre_commit/commands/hazmat.py new file mode 100644 index 000000000..01b27ce61 --- /dev/null +++ b/pre_commit/commands/hazmat.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +import argparse +import subprocess +from collections.abc import Sequence + +from pre_commit.parse_shebang import normalize_cmd + + +def add_parsers(parser: argparse.ArgumentParser) -> None: + subparsers = parser.add_subparsers(dest='tool') + + cd_parser = subparsers.add_parser( + 'cd', help='cd to a subdir and run the command', + ) + cd_parser.add_argument('subdir') + cd_parser.add_argument('cmd', nargs=argparse.REMAINDER) + + ignore_exit_code_parser = subparsers.add_parser( + 'ignore-exit-code', help='run the command but ignore the exit code', + ) + ignore_exit_code_parser.add_argument('cmd', nargs=argparse.REMAINDER) + + n1_parser = subparsers.add_parser( + 'n1', help='run the command once per filename', + ) + n1_parser.add_argument('cmd', nargs=argparse.REMAINDER) + + +def _cmd_filenames(cmd: tuple[str, ...]) -> tuple[ + tuple[str, ...], + tuple[str, ...], +]: + for idx, val in enumerate(reversed(cmd)): + if val == '--': + split = len(cmd) - idx + break + else: + raise SystemExit('hazmat entry must end with `--`') + + return cmd[:split - 1], cmd[split:] + + +def cd(subdir: str, cmd: tuple[str, ...]) -> int: + cmd, filenames = _cmd_filenames(cmd) + + prefix = f'{subdir}/' + new_filenames = [] + for filename in filenames: + if not filename.startswith(prefix): + raise SystemExit(f'unexpected file without {prefix=}: {filename}') + else: + new_filenames.append(filename.removeprefix(prefix)) + + cmd = normalize_cmd(cmd) + return subprocess.call((*cmd, *new_filenames), cwd=subdir) + + +def ignore_exit_code(cmd: tuple[str, ...]) -> int: + cmd = normalize_cmd(cmd) + subprocess.call(cmd) + return 0 + + +def n1(cmd: tuple[str, ...]) -> int: + cmd, filenames = _cmd_filenames(cmd) + cmd = normalize_cmd(cmd) + ret = 0 + for filename in filenames: + ret |= subprocess.call((*cmd, filename)) + return ret + + +def impl(args: argparse.Namespace) -> int: + args.cmd = tuple(args.cmd) + if args.tool == 'cd': + return cd(args.subdir, args.cmd) + elif args.tool == 'ignore-exit-code': + return ignore_exit_code(args.cmd) + elif args.tool == 'n1': + return n1(args.cmd) + else: + raise NotImplementedError(f'unexpected tool: {args.tool}') + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser() + add_parsers(parser) + args = parser.parse_args(argv) + + return impl(args) + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/pre_commit/lang_base.py b/pre_commit/lang_base.py index 95be7b9b3..198e93657 100644 --- a/pre_commit/lang_base.py +++ b/pre_commit/lang_base.py @@ -5,6 +5,7 @@ import random import re import shlex +import sys from collections.abc import Generator from collections.abc import Sequence from typing import Any @@ -171,7 +172,10 @@ def run_xargs( def hook_cmd(entry: str, args: Sequence[str]) -> tuple[str, ...]: - return (*shlex.split(entry), *args) + cmd = shlex.split(entry) + if cmd[:2] == ['pre-commit', 'hazmat']: + cmd = [sys.executable, '-m', 'pre_commit.commands.hazmat', *cmd[2:]] + return (*cmd, *args) def basic_run_hook( diff --git a/pre_commit/main.py b/pre_commit/main.py index c33fbfdaa..0c3eefdaa 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -10,6 +10,7 @@ from pre_commit import clientlib from pre_commit import git from pre_commit.color import add_color_option +from pre_commit.commands import hazmat from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.clean import clean from pre_commit.commands.gc import gc @@ -41,7 +42,7 @@ os.environ.pop('PYTHONEXECUTABLE', None) COMMANDS_NO_GIT = { - 'clean', 'gc', 'init-templatedir', 'sample-config', + 'clean', 'gc', 'hazmat', 'init-templatedir', 'sample-config', 'validate-config', 'validate-manifest', } @@ -245,6 +246,11 @@ def _add_cmd(name: str, *, help: str) -> argparse.ArgumentParser: _add_cmd('gc', help='Clean unused cached repos.') + hazmat_parser = _add_cmd( + 'hazmat', help='Composable tools for rare use in hook `entry`.', + ) + hazmat.add_parsers(hazmat_parser) + init_templatedir_parser = _add_cmd( 'init-templatedir', help=( @@ -389,6 +395,8 @@ def _add_cmd(name: str, *, help: str) -> argparse.ArgumentParser: return clean(store) elif args.command == 'gc': return gc(store) + elif args.command == 'hazmat': + return hazmat.impl(args) elif args.command == 'hook-impl': return hook_impl( store, diff --git a/tests/commands/hazmat_test.py b/tests/commands/hazmat_test.py new file mode 100644 index 000000000..df957e36e --- /dev/null +++ b/tests/commands/hazmat_test.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +import sys + +import pytest + +from pre_commit.commands.hazmat import _cmd_filenames +from pre_commit.commands.hazmat import main +from testing.util import cwd + + +def test_cmd_filenames_no_dash_dash(): + with pytest.raises(SystemExit) as excinfo: + _cmd_filenames(('no', 'dashdash', 'here')) + msg, = excinfo.value.args + assert msg == 'hazmat entry must end with `--`' + + +def test_cmd_filenames_no_filenames(): + cmd, filenames = _cmd_filenames(('hello', 'world', '--')) + assert cmd == ('hello', 'world') + assert filenames == () + + +def test_cmd_filenames_some_filenames(): + cmd, filenames = _cmd_filenames(('hello', 'world', '--', 'f1', 'f2')) + assert cmd == ('hello', 'world') + assert filenames == ('f1', 'f2') + + +def test_cmd_filenames_multiple_dashdash(): + cmd, filenames = _cmd_filenames(('hello', '--', 'arg', '--', 'f1', 'f2')) + assert cmd == ('hello', '--', 'arg') + assert filenames == ('f1', 'f2') + + +def test_cd_unexpected_filename(): + with pytest.raises(SystemExit) as excinfo: + main(('cd', 'subdir', 'cmd', '--', 'subdir/1', 'not-subdir/2')) + msg, = excinfo.value.args + assert msg == "unexpected file without prefix='subdir/': not-subdir/2" + + +def _norm(out): + return out.replace('\r\n', '\n') + + +def test_cd(tmp_path, capfd): + subdir = tmp_path.joinpath('subdir') + subdir.mkdir() + subdir.joinpath('a').write_text('a') + subdir.joinpath('b').write_text('b') + + with cwd(tmp_path): + ret = main(( + 'cd', 'subdir', + sys.executable, '-c', + 'import os; print(os.getcwd());' + 'import sys; [print(open(f).read()) for f in sys.argv[1:]]', + '--', + 'subdir/a', 'subdir/b', + )) + + assert ret == 0 + out, err = capfd.readouterr() + assert _norm(out) == f'{subdir}\na\nb\n' + assert err == '' + + +def test_ignore_exit_code(capfd): + ret = main(( + 'ignore-exit-code', sys.executable, '-c', 'raise SystemExit("bye")', + )) + assert ret == 0 + out, err = capfd.readouterr() + assert out == '' + assert _norm(err) == 'bye\n' + + +def test_n1(capfd): + ret = main(( + 'n1', sys.executable, '-c', 'import sys; print(sys.argv[1:])', + '--', + 'foo', 'bar', 'baz', + )) + assert ret == 0 + out, err = capfd.readouterr() + assert _norm(out) == "['foo']\n['bar']\n['baz']\n" + assert err == '' + + +def test_n1_some_error_code(): + ret = main(( + 'n1', sys.executable, '-c', + 'import sys; raise SystemExit(sys.argv[1] == "error")', + '--', + 'ok', 'error', 'ok', + )) + assert ret == 1 diff --git a/tests/lang_base_test.py b/tests/lang_base_test.py index da289aef8..9fac83da2 100644 --- a/tests/lang_base_test.py +++ b/tests/lang_base_test.py @@ -164,3 +164,15 @@ def test_basic_run_hook(tmp_path): assert ret == 0 out = out.replace(b'\r\n', b'\n') assert out == b'hi hello file file file\n' + + +def test_hook_cmd(): + assert lang_base.hook_cmd('echo hi', ()) == ('echo', 'hi') + + +def test_hook_cmd_hazmat(): + ret = lang_base.hook_cmd('pre-commit hazmat cd a echo -- b', ()) + assert ret == ( + sys.executable, '-m', 'pre_commit.commands.hazmat', + 'cd', 'a', 'echo', '--', 'b', + ) diff --git a/tests/main_test.py b/tests/main_test.py index 945349fa4..eb9ea18d8 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -8,6 +8,7 @@ import pre_commit.constants as C from pre_commit import main +from pre_commit.commands import hazmat from pre_commit.errors import FatalError from pre_commit.util import cmd_output from testing.auto_namedtuple import auto_namedtuple @@ -158,6 +159,17 @@ def test_all_cmds(command, mock_commands, mock_store_dir): assert_only_one_mock_called(mock_commands) +def test_hazmat(mock_store_dir): + with mock.patch.object(hazmat, 'impl') as mck: + main.main(('hazmat', 'cd', 'subdir', '--', 'cmd', '--', 'f1', 'f2')) + assert mck.call_count == 1 + (arg,), dct = mck.call_args + assert dct == {} + assert arg.tool == 'cd' + assert arg.subdir == 'subdir' + assert arg.cmd == ['cmd', '--', 'f1', 'f2'] + + def test_try_repo(mock_store_dir): with mock.patch.object(main, 'try_repo') as patch: main.main(('try-repo', '.')) diff --git a/tests/repository_test.py b/tests/repository_test.py index b1c7a0024..5d71c3e4c 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -506,3 +506,14 @@ def test_args_with_spaces_and_quotes(tmp_path): expected = b"['i have spaces', 'and\"\\'quotes', '$and !this']\n" assert ret == (0, expected) + + +def test_hazmat(tmp_path): + ret = run_language( + tmp_path, unsupported, + f'pre-commit hazmat ignore-exit-code {shlex.quote(sys.executable)} ' + f"-c 'import sys; raise SystemExit(sys.argv[1:])'", + ('f1', 'f2'), + ) + expected = b"['f1', 'f2']\n" + assert ret == (0, expected) From 1af6c8fa9502336c6977c2ff3e79185bd97a6e57 Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Sat, 22 Nov 2025 16:02:16 -0500 Subject: [PATCH 960/967] v4.5.0 --- CHANGELOG.md | 7 +++++++ setup.cfg | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b27af5e7e..1434728d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +4.5.0 - 2025-11-22 +================== + +### Features +- Add `pre-commit hazmat`. + - #3585 PR by @asottile. + 4.4.0 - 2025-11-08 ================== diff --git a/setup.cfg b/setup.cfg index be031c3ef..00c71759a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 4.4.0 +version = 4.5.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 8ea2b790d817088444b2328ff6cfe6742260070f Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Sat, 22 Nov 2025 16:06:27 -0500 Subject: [PATCH 961/967] remove sha256 file from zipapp script github displays the checksum for us now! --- testing/zipapp/make | 3 --- 1 file changed, 3 deletions(-) diff --git a/testing/zipapp/make b/testing/zipapp/make index 165046f66..43bb4373f 100755 --- a/testing/zipapp/make +++ b/testing/zipapp/make @@ -107,9 +107,6 @@ def main() -> int: shebang = '/usr/bin/env python3' zipapp.create_archive(tmpdir, filename, interpreter=shebang) - with open(f'{filename}.sha256sum', 'w') as f: - subprocess.check_call(('sha256sum', filename), stdout=f) - return 0 From 465192d7de58d569776eaaa818c94cb2b962d436 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 20:53:38 +0000 Subject: [PATCH 962/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.21.1 → v3.21.2](https://github.com/asottile/pyupgrade/compare/v3.21.1...v3.21.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e47d56ca3..50893030f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.21.1 + rev: v3.21.2 hooks: - id: pyupgrade args: [--py310-plus] From 48953556d06f8cdb4248002c1a0044e69e0916b3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 21:05:15 +0000 Subject: [PATCH 963/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.18.2 → v1.19.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.18.2...v1.19.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 50893030f..cedeae5e8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.18.2 + rev: v1.19.0 hooks: - id: mypy additional_dependencies: [types-pyyaml] From c251e6b6d011b3b262339dc8e109de29b0ff8db1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 20:48:45 +0000 Subject: [PATCH 964/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.19.0 → v1.19.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.19.0...v1.19.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cedeae5e8..83ff03f3d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.19.0 + rev: v1.19.1 hooks: - id: mypy additional_dependencies: [types-pyyaml] From 51592eececd13b99c40ec477ad8f810799147227 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 16 Dec 2025 15:45:01 -0500 Subject: [PATCH 965/967] fix python local template when artifact dirs are present --- pre_commit/resources/empty_template_setup.py | 2 +- tests/languages/python_test.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pre_commit/resources/empty_template_setup.py b/pre_commit/resources/empty_template_setup.py index ef05eef84..e8b1ff02c 100644 --- a/pre_commit/resources/empty_template_setup.py +++ b/pre_commit/resources/empty_template_setup.py @@ -1,4 +1,4 @@ from setuptools import setup -setup(name='pre-commit-placeholder-package', version='0.0.0') +setup(name='pre-commit-placeholder-package', version='0.0.0', py_modules=[]) diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 565525a40..593634b79 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -10,6 +10,8 @@ from pre_commit.envcontext import envcontext from pre_commit.languages import python from pre_commit.prefix import Prefix +from pre_commit.store import _make_local_repo +from pre_commit.util import cmd_output_b from pre_commit.util import make_executable from pre_commit.util import win_exe from testing.auto_namedtuple import auto_namedtuple @@ -351,3 +353,15 @@ def test_python_hook_weird_setup_cfg(tmp_path): ret = run_language(tmp_path, python, 'socks', [os.devnull]) assert ret == (0, f'[{os.devnull!r}]\nhello hello\n'.encode()) + + +def test_local_repo_with_other_artifacts(tmp_path): + cmd_output_b('git', 'init', tmp_path) + _make_local_repo(str(tmp_path)) + # pretend a rust install also ran here + tmp_path.joinpath('target').mkdir() + + ret, out = run_language(tmp_path, python, 'python --version') + + assert ret == 0 + assert out.startswith(b'Python ') From 8a0630ca1aa7f6d5665effe674ebe2022af17919 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 16 Dec 2025 16:13:56 -0500 Subject: [PATCH 966/967] v4.5.1 --- CHANGELOG.md | 7 +++++++ setup.cfg | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1434728d0..879ae0731 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +4.5.1 - 2025-12-16 +================== + +### Fixes +- Fix `language: python` with `repo: local` without `additional_dependencies`. + - #3597 PR by @asottile. + 4.5.0 - 2025-11-22 ================== diff --git a/setup.cfg b/setup.cfg index 00c71759a..a95ee4473 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 4.5.0 +version = 4.5.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 37a879e65ee00d8375d12f053ef76e0024a0ed55 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Dec 2025 20:26:26 +0000 Subject: [PATCH 967/967] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v3.1.0 → v3.2.0](https://github.com/asottile/setup-cfg-fmt/compare/v3.1.0...v3.2.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 83ff03f3d..3654066f4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v3.1.0 + rev: v3.2.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports

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%0AcDMh%UGT>#`||~+1FOtBlZ%SrXoNM@14%S& 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 BOpgEn From 58a190fd69d43c420195b0de5f16345115d1a071 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 23 Sep 2020 17:44:18 -0700 Subject: [PATCH 417/967] don't pass through -p if using the default version --- pre_commit/languages/python.py | 16 ++++++++-------- tests/languages/python_test.py | 3 ++- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index afa093d56..65f521cdc 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -132,13 +132,11 @@ def _sys_executable_matches(version: str) -> bool: return sys.version_info[:len(info)] == info -def norm_version(version: str) -> str: - if version == C.DEFAULT: - return os.path.realpath(sys.executable) - - # first see if our current executable is appropriate - if _sys_executable_matches(version): - return sys.executable +def norm_version(version: str) -> Optional[str]: + if version == C.DEFAULT: # use virtualenv's default + return None + elif _sys_executable_matches(version): # virtualenv defaults to our exe + return None if os.name == 'nt': # pragma: no cover (windows) version_exec = _find_by_py_launcher(version) @@ -194,8 +192,10 @@ def install_environment( additional_dependencies: Sequence[str], ) -> None: envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) + venv_cmd = [sys.executable, '-mvirtualenv', envdir] python = norm_version(version) - venv_cmd = (sys.executable, '-mvirtualenv', envdir, '-p', python) + if python is not None: + venv_cmd.extend(('-p', python)) install_cmd = ('python', '-mpip', 'install', '.', *additional_dependencies) with clean_path_on_failure(envdir): diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 29c5a9bf2..cfe14834f 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -36,13 +36,14 @@ def test_norm_version_expanduser(): def test_norm_version_of_default_is_sys_executable(): - assert python.norm_version('default') == os.path.realpath(sys.executable) + assert python.norm_version('default') is None @pytest.mark.parametrize('v', ('python3.6', 'python3', 'python')) def test_sys_executable_matches(v): with mock.patch.object(sys, 'version_info', (3, 6, 7)): assert python._sys_executable_matches(v) + assert python.norm_version(v) is None @pytest.mark.parametrize('v', ('notpython', 'python3.x')) From 3de3c6a5fcaa0eb2a63123a343e3ec44da199b1e Mon Sep 17 00:00:00 2001 From: Maximilian Cosmo Sitter <48606431+mcsitter@users.noreply.github.com> Date: Wed, 23 Sep 2020 02:20:11 +0200 Subject: [PATCH 418/967] Update pre-commit version in sample config --- pre_commit/commands/sample_config.py | 2 +- tests/commands/sample_config_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/sample_config.py b/pre_commit/commands/sample_config.py index d435faa8c..64617c333 100644 --- a/pre_commit/commands/sample_config.py +++ b/pre_commit/commands/sample_config.py @@ -7,7 +7,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.4.0 + rev: v3.2.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/tests/commands/sample_config_test.py b/tests/commands/sample_config_test.py index 11c087649..8e3a9043f 100644 --- a/tests/commands/sample_config_test.py +++ b/tests/commands/sample_config_test.py @@ -10,7 +10,7 @@ def test_sample_config(capsys): # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.4.0 + rev: v3.2.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer From 003e4c21e00323462fd26d9393f8af10a2e1cbe8 Mon Sep 17 00:00:00 2001 From: Ruairidh MacLeod Date: Thu, 10 Sep 2020 11:36:10 +0000 Subject: [PATCH 419/967] add initial dotnet support --- .gitignore | 1 + pre_commit/languages/all.py | 2 + pre_commit/languages/dotnet.py | 90 +++++++++++++++++++ testing/gen-languages-all | 5 +- .../dotnet_hooks_csproj_repo/.gitignore | 3 + .../.pre-commit-hooks.yaml | 5 ++ .../dotnet_hooks_csproj_repo/Program.cs | 12 +++ .../dotnet_hooks_csproj_repo.csproj | 9 ++ .../dotnet_hooks_sln_repo/.gitignore | 3 + .../.pre-commit-hooks.yaml | 5 ++ .../dotnet_hooks_sln_repo/Program.cs | 12 +++ .../dotnet_hooks_sln_repo.csproj | 9 ++ .../dotnet_hooks_sln_repo.sln | 34 +++++++ tests/languages/dotnet_test.py | 0 tests/repository_test.py | 14 +++ tox.ini | 2 +- 16 files changed, 203 insertions(+), 3 deletions(-) create mode 100644 pre_commit/languages/dotnet.py create mode 100644 testing/resources/dotnet_hooks_csproj_repo/.gitignore create mode 100644 testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml create mode 100644 testing/resources/dotnet_hooks_csproj_repo/Program.cs create mode 100644 testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj create mode 100644 testing/resources/dotnet_hooks_sln_repo/.gitignore create mode 100644 testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml create mode 100644 testing/resources/dotnet_hooks_sln_repo/Program.cs create mode 100644 testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj create mode 100644 testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.sln create mode 100644 tests/languages/dotnet_test.py diff --git a/.gitignore b/.gitignore index 5428b0ad8..4f4f6b941 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ /.tox /dist /venv* +.vscode/ diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 5609631b0..f32780c14 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -8,6 +8,7 @@ from pre_commit.languages import conda from pre_commit.languages import docker from pre_commit.languages import docker_image +from pre_commit.languages import dotnet from pre_commit.languages import fail from pre_commit.languages import golang from pre_commit.languages import node @@ -42,6 +43,7 @@ class Language(NamedTuple): 'conda': Language(name='conda', ENVIRONMENT_DIR=conda.ENVIRONMENT_DIR, get_default_version=conda.get_default_version, healthy=conda.healthy, install_environment=conda.install_environment, run_hook=conda.run_hook), # noqa: E501 'docker': Language(name='docker', ENVIRONMENT_DIR=docker.ENVIRONMENT_DIR, get_default_version=docker.get_default_version, healthy=docker.healthy, install_environment=docker.install_environment, run_hook=docker.run_hook), # noqa: E501 'docker_image': Language(name='docker_image', ENVIRONMENT_DIR=docker_image.ENVIRONMENT_DIR, get_default_version=docker_image.get_default_version, healthy=docker_image.healthy, install_environment=docker_image.install_environment, run_hook=docker_image.run_hook), # noqa: E501 + 'dotnet': Language(name='dotnet', ENVIRONMENT_DIR=dotnet.ENVIRONMENT_DIR, get_default_version=dotnet.get_default_version, healthy=dotnet.healthy, install_environment=dotnet.install_environment, run_hook=dotnet.run_hook), # noqa: E501 'fail': Language(name='fail', ENVIRONMENT_DIR=fail.ENVIRONMENT_DIR, get_default_version=fail.get_default_version, healthy=fail.healthy, install_environment=fail.install_environment, run_hook=fail.run_hook), # noqa: E501 'golang': Language(name='golang', ENVIRONMENT_DIR=golang.ENVIRONMENT_DIR, get_default_version=golang.get_default_version, healthy=golang.healthy, install_environment=golang.install_environment, run_hook=golang.run_hook), # noqa: E501 'node': Language(name='node', ENVIRONMENT_DIR=node.ENVIRONMENT_DIR, get_default_version=node.get_default_version, healthy=node.healthy, install_environment=node.install_environment, run_hook=node.run_hook), # noqa: E501 diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py new file mode 100644 index 000000000..a8abc8611 --- /dev/null +++ b/pre_commit/languages/dotnet.py @@ -0,0 +1,90 @@ +import contextlib +import os.path +from typing import Generator +from typing import Sequence +from typing import Tuple + +import pre_commit.constants as C +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import Var +from pre_commit.hook import Hook +from pre_commit.languages import helpers +from pre_commit.prefix import Prefix +from pre_commit.util import clean_path_on_failure +from pre_commit.util import rmtree + +ENVIRONMENT_DIR = 'dotnetenv' +BIN_DIR = 'bin' + +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy + + +def get_env_patch(venv: str) -> PatchesT: + return ( + ('PATH', (os.path.join(venv, BIN_DIR), os.pathsep, Var('PATH'))), + ) + + +@contextlib.contextmanager +def in_env(prefix: Prefix) -> Generator[None, None, None]: + directory = helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT) + envdir = prefix.path(directory) + with envcontext(get_env_patch(envdir)): + yield + + +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: + helpers.assert_version_default('dotnet', version) + helpers.assert_no_additional_deps('dotnet', additional_dependencies) + + envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) + with clean_path_on_failure(envdir): + build_dir = 'pre-commit-build' + + # Build & pack nupkg file + helpers.run_setup_cmd( + prefix, + ( + 'dotnet', 'pack', + '--configuration', 'Release', + '--output', build_dir, + ), + ) + + # Determine tool from the packaged file ..nupkg + build_outputs = os.listdir(os.path.join(prefix.prefix_dir, build_dir)) + if len(build_outputs) != 1: + raise NotImplementedError( + f"Can't handle multiple build outputs. Got {build_outputs}", + ) + tool_name = build_outputs[0].split('.')[0] + + # Install to bin dir + helpers.run_setup_cmd( + prefix, + ( + 'dotnet', 'tool', 'install', + '--tool-path', os.path.join(envdir, BIN_DIR), + '--add-source', build_dir, + tool_name, + ), + ) + + # Cleanup build output + for d in ('bin', 'obj', build_dir): + rmtree(prefix.path(d)) + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: + with in_env(hook.prefix): + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/testing/gen-languages-all b/testing/gen-languages-all index 2bff7beb0..35eac042b 100755 --- a/testing/gen-languages-all +++ b/testing/gen-languages-all @@ -2,8 +2,9 @@ import sys LANGUAGES = [ - 'conda', 'docker', 'docker_image', 'fail', 'golang', 'node', 'perl', - 'pygrep', 'python', 'ruby', 'rust', 'script', 'swift', 'system', + 'conda', 'docker', 'dotnet', 'docker_image', 'fail', 'golang', + 'node', 'perl', 'pygrep', 'python', 'ruby', 'rust', 'script', 'swift', + 'system', ] FIELDS = [ 'ENVIRONMENT_DIR', 'get_default_version', 'healthy', 'install_environment', diff --git a/testing/resources/dotnet_hooks_csproj_repo/.gitignore b/testing/resources/dotnet_hooks_csproj_repo/.gitignore new file mode 100644 index 000000000..edcd28f4a --- /dev/null +++ b/testing/resources/dotnet_hooks_csproj_repo/.gitignore @@ -0,0 +1,3 @@ +bin/ +obj/ +nupkg/ diff --git a/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml new file mode 100644 index 000000000..d005a74cc --- /dev/null +++ b/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml @@ -0,0 +1,5 @@ +- id: dotnet example hook + name: dotnet example hook + entry: testeroni + language: dotnet + files: '' diff --git a/testing/resources/dotnet_hooks_csproj_repo/Program.cs b/testing/resources/dotnet_hooks_csproj_repo/Program.cs new file mode 100644 index 000000000..1456e8ef2 --- /dev/null +++ b/testing/resources/dotnet_hooks_csproj_repo/Program.cs @@ -0,0 +1,12 @@ +using System; + +namespace dotnet_hooks_repo +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello from dotnet!"); + } + } +} diff --git a/testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj b/testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj new file mode 100644 index 000000000..d2e556ac0 --- /dev/null +++ b/testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj @@ -0,0 +1,9 @@ + + + Exe + netcoreapp3.1 + true + testeroni + ./nupkg + + diff --git a/testing/resources/dotnet_hooks_sln_repo/.gitignore b/testing/resources/dotnet_hooks_sln_repo/.gitignore new file mode 100644 index 000000000..edcd28f4a --- /dev/null +++ b/testing/resources/dotnet_hooks_sln_repo/.gitignore @@ -0,0 +1,3 @@ +bin/ +obj/ +nupkg/ diff --git a/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml new file mode 100644 index 000000000..d005a74cc --- /dev/null +++ b/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml @@ -0,0 +1,5 @@ +- id: dotnet example hook + name: dotnet example hook + entry: testeroni + language: dotnet + files: '' diff --git a/testing/resources/dotnet_hooks_sln_repo/Program.cs b/testing/resources/dotnet_hooks_sln_repo/Program.cs new file mode 100644 index 000000000..04ad4e0cc --- /dev/null +++ b/testing/resources/dotnet_hooks_sln_repo/Program.cs @@ -0,0 +1,12 @@ +using System; + +namespace dotnet_hooks_sln_repo +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello from dotnet!"); + } + } +} diff --git a/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj b/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj new file mode 100644 index 000000000..e37296480 --- /dev/null +++ b/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj @@ -0,0 +1,9 @@ + + + Exe + netcoreapp3.1 + true + testeroni + ./nupkg + + diff --git a/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.sln b/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.sln new file mode 100644 index 000000000..87d2afbaf --- /dev/null +++ b/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet_hooks_sln_repo", "dotnet_hooks_sln_repo.csproj", "{6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x64.ActiveCfg = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x64.Build.0 = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x86.ActiveCfg = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x86.Build.0 = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|Any CPU.Build.0 = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x64.ActiveCfg = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x64.Build.0 = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x86.ActiveCfg = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/tests/languages/dotnet_test.py b/tests/languages/dotnet_test.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/repository_test.py b/tests/repository_test.py index 035b02a65..3f7a39fbf 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -917,3 +917,17 @@ def test_local_perl_additional_dependencies(store): ret, out = _hook_run(hook, (), color=False) assert ret == 0 assert _norm_out(out).startswith(b'This is perltidy, v20200110') + + +@pytest.mark.parametrize( + 'repo', + ( + 'dotnet_hooks_csproj_repo', + 'dotnet_hooks_sln_repo', + ), +) +def test_dotnet_hook(tempdir_factory, store, repo): + _test_hook_repo( + tempdir_factory, store, repo, + 'dotnet example hook', [], b'Hello from dotnet!\n', + ) diff --git a/tox.ini b/tox.ini index 63a3aab81..11b20d418 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist = py36,py37,py38,pypy3,pre-commit [testenv] deps = -rrequirements-dev.txt -passenv = HOME LOCALAPPDATA RUSTUP_HOME +passenv = APPDATA HOME LOCALAPPDATA PROGRAMFILES RUSTUP_HOME commands = coverage erase coverage run -m pytest {posargs:tests} From bc198b89ca9a991dd1a09670c3ed823872b232aa Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 23 Sep 2020 15:35:41 -0700 Subject: [PATCH 420/967] add zipapp support --- testing/zipapp/Dockerfile | 14 +++++++ testing/zipapp/entry | 66 +++++++++++++++++++++++++++++ testing/zipapp/fakepython | 45 ++++++++++++++++++++ testing/zipapp/make | 88 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 213 insertions(+) create mode 100644 testing/zipapp/Dockerfile create mode 100755 testing/zipapp/entry create mode 100755 testing/zipapp/fakepython create mode 100755 testing/zipapp/make diff --git a/testing/zipapp/Dockerfile b/testing/zipapp/Dockerfile new file mode 100644 index 000000000..e21d5fe31 --- /dev/null +++ b/testing/zipapp/Dockerfile @@ -0,0 +1,14 @@ +FROM ubuntu:bionic +RUN : \ + && apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + python3 \ + python3-distutils \ + python3-venv \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +ENV LANG=C.UTF-8 PATH=/venv/bin:$PATH +RUN : \ + && python3.6 -mvenv /venv \ + && pip install --no-cache-dir pip setuptools wheel no-manylinux --upgrade diff --git a/testing/zipapp/entry b/testing/zipapp/entry new file mode 100755 index 000000000..73a984d49 --- /dev/null +++ b/testing/zipapp/entry @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +import os.path +import shutil +import stat +import sys +import tempfile +import zipfile + +from pre_commit.file_lock import lock + +CACHE_DIR = os.path.expanduser('~/.cache/pre-commit-zipapp') + + +def _make_executable(filename: str) -> None: + os.chmod(filename, os.stat(filename).st_mode | stat.S_IXUSR) + + +def _ensure_cache(zipf: zipfile.ZipFile, cache_key: str) -> str: + os.makedirs(CACHE_DIR, exist_ok=True) + + cache_dest = os.path.join(CACHE_DIR, cache_key) + lock_filename = os.path.join(CACHE_DIR, f'{cache_key}.lock') + + if os.path.exists(cache_dest): + return cache_dest + + with lock(lock_filename, blocked_cb=lambda: None): + # another process may have completed this work + if os.path.exists(cache_dest): + return cache_dest + + tmpdir = tempfile.mkdtemp(prefix=os.path.join(CACHE_DIR, '')) + try: + zipf.extractall(tmpdir) + # zip doesn't maintain permissions + _make_executable(os.path.join(tmpdir, 'fakepython')) + os.rename(tmpdir, cache_dest) + except BaseException: + shutil.rmtree(tmpdir) + raise + + return cache_dest + + +def main() -> int: + with zipfile.ZipFile(os.path.dirname(__file__)) as zipf: + with zipf.open('CACHE_KEY') as f: + cache_key = f.read().decode().strip() + + cache_dest = _ensure_cache(zipf, cache_key) + + fakepython = os.path.join(cache_dest, 'fakepython') + cmd = (sys.executable, fakepython, '-mpre_commit', *sys.argv[1:]) + if sys.platform == 'win32': # https://bugs.python.org/issue19124 + import subprocess + + if sys.version_info < (3, 7): # https://bugs.python.org/issue25942 + return subprocess.Popen(cmd).wait() + else: + return subprocess.call(cmd) + else: + os.execvp(cmd[0], cmd) + + +if __name__ == '__main__': + exit(main()) diff --git a/testing/zipapp/fakepython b/testing/zipapp/fakepython new file mode 100755 index 000000000..e437d1df5 --- /dev/null +++ b/testing/zipapp/fakepython @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""A shim executable to put dependencies on sys.path""" +import argparse +import os.path +import runpy +import sys + +HERE = os.path.dirname(os.path.realpath(__file__)) +WHEELDIR = os.path.join(HERE, 'wheels') +SITE_DIRS = frozenset(('dist-packages', 'site-packages')) + + +def main() -> int: + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument('-m') + args, rest = parser.parse_known_args() + + if args.m: + # try and remove site-packages from sys.path so our packages win + sys.path[:] = [ + p for p in sys.path + if os.path.split(p)[1] not in SITE_DIRS + ] + for wheel in sorted(os.listdir(WHEELDIR)): + sys.path.append(os.path.join(WHEELDIR, wheel)) + if args.m == 'pre_commit' or args.m.startswith('pre_commit.'): + sys.executable = os.path.abspath(__file__) + sys.argv[1:] = rest + runpy.run_module(args.m, run_name='__main__', alter_sys=True) + return 0 + else: + cmd = (sys.executable, *sys.argv[1:]) + if sys.platform == 'win32': # https://bugs.python.org/issue19124 + import subprocess + + if sys.version_info < (3, 7): # https://bugs.python.org/issue25942 + return subprocess.Popen(cmd).wait() + else: + return subprocess.call(cmd) + else: + os.execvp(cmd[0], cmd) + + +if __name__ == '__main__': + exit(main()) diff --git a/testing/zipapp/make b/testing/zipapp/make new file mode 100755 index 000000000..752768de6 --- /dev/null +++ b/testing/zipapp/make @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +import argparse +import base64 +import hashlib +import os.path +import shutil +import subprocess +import tempfile +import zipapp +import zipfile + +HERE = os.path.dirname(os.path.realpath(__file__)) +IMG = 'make-pre-commit-zipapp' + + +def _msg(s: str) -> None: + print(f'\033[7m{s}\033[m') + + +def _exit_if_retv(*cmd: str) -> None: + if subprocess.call(cmd): + raise SystemExit(1) + + +def _check_no_shared_objects(wheeldir: str) -> None: + for zip_filename in os.listdir(wheeldir): + with zipfile.ZipFile(os.path.join(wheeldir, zip_filename)) as zipf: + for filename in zipf.namelist(): + if filename.endswith('.so') or '.so.' in filename: + raise AssertionError(zip_filename, filename) + + +def _write_cache_key(version: str, wheeldir: str, dest: str) -> None: + cache_hash = hashlib.sha256(f'{version}\n'.encode()) + for filename in sorted(os.listdir(wheeldir)): + cache_hash.update(f'{filename}\n'.encode()) + with open(os.path.join(HERE, 'fakepython'), 'rb') as f: + cache_hash.update(f.read()) + with open(os.path.join(dest, 'CACHE_KEY'), 'wb') as f: + f.write(base64.urlsafe_b64encode(cache_hash.digest()).rstrip(b'=')) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument('version') + args = parser.parse_args() + + with tempfile.TemporaryDirectory() as tmpdir: + wheeldir = os.path.join(tmpdir, 'wheels') + os.mkdir(wheeldir) + + _msg('building podman image...') + _exit_if_retv('podman', 'build', '-q', '-t', IMG, HERE) + + _msg('populating wheels...') + _exit_if_retv( + 'podman', 'run', '--rm', '--volume', f'{wheeldir}:/wheels:rw', IMG, + 'pip', 'wheel', f'pre_commit=={args.version}', + '--wheel-dir', '/wheels', + ) + + _msg('validating wheels...') + _check_no_shared_objects(wheeldir) + + _msg('adding fakepython / __main__.py...') + shutil.copy(os.path.join(HERE, 'fakepython'), tmpdir) + mainfile = os.path.join(tmpdir, '__main__.py') + shutil.copy(os.path.join(HERE, 'entry'), mainfile) + + _msg('copying file_lock.py...') + file_lock_py = os.path.join(HERE, '../../pre_commit/file_lock.py') + file_lock_py_dest = os.path.join(tmpdir, 'pre_commit/file_lock.py') + os.makedirs(os.path.dirname(file_lock_py_dest)) + shutil.copy(file_lock_py, file_lock_py_dest) + + _msg('writing CACHE_KEY...') + _write_cache_key(args.version, wheeldir, tmpdir) + + filename = f'pre-commit-{args.version}.pyz' + _msg(f'writing {filename}...') + shebang = '/usr/bin/env python3' + zipapp.create_archive(tmpdir, filename, interpreter=shebang) + + return 0 + + +if __name__ == '__main__': + exit(main()) From fbd529204bac42c922094552578c79788a26ebe7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 28 Sep 2020 18:37:10 -0700 Subject: [PATCH 421/967] make an exe stub for windows --- requirements-dev.txt | 1 + testing/zipapp/entry | 11 ++++++++--- testing/zipapp/make | 24 +++++++++++++++++++++--- testing/zipapp/{fakepython => python} | 7 +++++-- 4 files changed, 35 insertions(+), 8 deletions(-) rename testing/zipapp/{fakepython => python} (85%) diff --git a/requirements-dev.txt b/requirements-dev.txt index 14ada96ed..56afd41f5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,6 @@ covdefaults coverage +distlib pytest pytest-env re-assert diff --git a/testing/zipapp/entry b/testing/zipapp/entry index 73a984d49..f0a345e6a 100755 --- a/testing/zipapp/entry +++ b/testing/zipapp/entry @@ -33,7 +33,8 @@ def _ensure_cache(zipf: zipfile.ZipFile, cache_key: str) -> str: try: zipf.extractall(tmpdir) # zip doesn't maintain permissions - _make_executable(os.path.join(tmpdir, 'fakepython')) + _make_executable(os.path.join(tmpdir, 'python')) + _make_executable(os.path.join(tmpdir, 'python.exe')) os.rename(tmpdir, cache_dest) except BaseException: shutil.rmtree(tmpdir) @@ -49,8 +50,12 @@ def main() -> int: cache_dest = _ensure_cache(zipf, cache_key) - fakepython = os.path.join(cache_dest, 'fakepython') - cmd = (sys.executable, fakepython, '-mpre_commit', *sys.argv[1:]) + if sys.platform != 'win32': + exe = os.path.join(cache_dest, 'python') + else: + exe = os.path.join(cache_dest, 'python.exe') + + cmd = (exe, '-mpre_commit', *sys.argv[1:]) if sys.platform == 'win32': # https://bugs.python.org/issue19124 import subprocess diff --git a/testing/zipapp/make b/testing/zipapp/make index 752768de6..a644946d5 100755 --- a/testing/zipapp/make +++ b/testing/zipapp/make @@ -2,6 +2,8 @@ import argparse import base64 import hashlib +import importlib.resources +import io import os.path import shutil import subprocess @@ -30,11 +32,25 @@ def _check_no_shared_objects(wheeldir: str) -> None: raise AssertionError(zip_filename, filename) +def _add_shim(dest: str) -> None: + shim = os.path.join(HERE, 'python') + shutil.copy(shim, dest) + + bio = io.BytesIO() + with zipfile.ZipFile(bio, 'w') as zipf: + zipf.write(shim, arcname='__main__.py') + + with open(os.path.join(dest, 'python.exe'), 'wb') as f: + f.write(importlib.resources.read_binary('distlib', 't32.exe')) + f.write(b'#!py.exe -3\n') + f.write(bio.getvalue()) + + def _write_cache_key(version: str, wheeldir: str, dest: str) -> None: cache_hash = hashlib.sha256(f'{version}\n'.encode()) for filename in sorted(os.listdir(wheeldir)): cache_hash.update(f'{filename}\n'.encode()) - with open(os.path.join(HERE, 'fakepython'), 'rb') as f: + with open(os.path.join(HERE, 'python'), 'rb') as f: cache_hash.update(f.read()) with open(os.path.join(dest, 'CACHE_KEY'), 'wb') as f: f.write(base64.urlsafe_b64encode(cache_hash.digest()).rstrip(b'=')) @@ -62,11 +78,13 @@ def main() -> int: _msg('validating wheels...') _check_no_shared_objects(wheeldir) - _msg('adding fakepython / __main__.py...') - shutil.copy(os.path.join(HERE, 'fakepython'), tmpdir) + _msg('adding __main__.py...') mainfile = os.path.join(tmpdir, '__main__.py') shutil.copy(os.path.join(HERE, 'entry'), mainfile) + _msg('adding shim...') + _add_shim(tmpdir) + _msg('copying file_lock.py...') file_lock_py = os.path.join(HERE, '../../pre_commit/file_lock.py') file_lock_py_dest = os.path.join(tmpdir, 'pre_commit/file_lock.py') diff --git a/testing/zipapp/fakepython b/testing/zipapp/python similarity index 85% rename from testing/zipapp/fakepython rename to testing/zipapp/python index e437d1df5..97c5928e3 100755 --- a/testing/zipapp/fakepython +++ b/testing/zipapp/python @@ -5,7 +5,10 @@ import os.path import runpy import sys -HERE = os.path.dirname(os.path.realpath(__file__)) +# an exe-zipapp will have a __file__ of shim.exe/__main__.py +EXE = __file__ if os.path.isfile(__file__) else os.path.dirname(__file__) +EXE = os.path.realpath(EXE) +HERE = os.path.dirname(EXE) WHEELDIR = os.path.join(HERE, 'wheels') SITE_DIRS = frozenset(('dist-packages', 'site-packages')) @@ -24,7 +27,7 @@ def main() -> int: for wheel in sorted(os.listdir(WHEELDIR)): sys.path.append(os.path.join(WHEELDIR, wheel)) if args.m == 'pre_commit' or args.m.startswith('pre_commit.'): - sys.executable = os.path.abspath(__file__) + sys.executable = EXE sys.argv[1:] = rest runpy.run_module(args.m, run_name='__main__', alter_sys=True) return 0 From 32a286d5300a84bc8fcbcc87f4d88171f9701354 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 10 Oct 2020 16:39:10 -0700 Subject: [PATCH 422/967] use implementation-agnostic conda package for test --- tests/repository_test.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/repository_test.py b/tests/repository_test.py index 3f7a39fbf..a6d801ec1 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -94,8 +94,8 @@ def test_conda_with_additional_dependencies_hook(tempdir_factory, store): config_kwargs={ 'hooks': [{ 'id': 'additional-deps', - 'args': ['-c', 'import mccabe; print("OK")'], - 'additional_dependencies': ['mccabe'], + 'args': ['-c', 'import tzdata; print("OK")'], + 'additional_dependencies': ['python-tzdata'], }], }, ) @@ -109,8 +109,8 @@ def test_local_conda_additional_dependencies(store): 'name': 'local-conda', 'entry': 'python', 'language': 'conda', - 'args': ['-c', 'import mccabe; print("OK")'], - 'additional_dependencies': ['mccabe'], + 'args': ['-c', 'import tzdata; print("OK")'], + 'additional_dependencies': ['python-tzdata'], }], } hook = _get_hook(config, store, 'local-conda') From 3584b99caac8cc7320cb7fd0fb23fff8f04d0281 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 9 Oct 2020 11:31:12 -0700 Subject: [PATCH 423/967] simplify docker run --- pre_commit/languages/docker.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 9c1311988..9d30568c5 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -87,9 +87,8 @@ def run_hook( # automated cleanup of docker images. build_docker_image(hook.prefix, pull=False) - hook_cmd = hook.cmd - entry_exe, cmd_rest = hook.cmd[0], hook_cmd[1:] + entry_exe, *cmd_rest = hook.cmd entry_tag = ('--entrypoint', entry_exe, docker_tag(hook.prefix)) - cmd = docker_cmd() + entry_tag + cmd_rest + cmd = (*docker_cmd(), *entry_tag, *cmd_rest) return helpers.run_xargs(hook, cmd, file_args, color=color) From 2fc676709d1caf2488296b915104530439f8a190 Mon Sep 17 00:00:00 2001 From: Marco Gorelli Date: Wed, 14 Oct 2020 18:15:22 +0100 Subject: [PATCH 424/967] Remove unnecessary fixtures in signatures from pygrep tests --- tests/languages/pygrep_test.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/languages/pygrep_test.py b/tests/languages/pygrep_test.py index cabea22ec..6eef56b7a 100644 --- a/tests/languages/pygrep_test.py +++ b/tests/languages/pygrep_test.py @@ -23,42 +23,47 @@ def some_files(tmpdir): ("h'q", 1, "f3:1:with'quotes\n"), ), ) -def test_main(some_files, cap_out, pattern, expected_retcode, expected_out): +def test_main(cap_out, pattern, expected_retcode, expected_out): ret = pygrep.main((pattern, 'f1', 'f2', 'f3')) out = cap_out.get() assert ret == expected_retcode assert out == expected_out -def test_ignore_case(some_files, cap_out): +@pytest.mark.usefixtures('some_files') +def test_ignore_case(cap_out): ret = pygrep.main(('--ignore-case', 'info', 'f1', 'f2', 'f3')) out = cap_out.get() assert ret == 1 assert out == 'f2:1:[INFO] hi\n' -def test_multiline(some_files, cap_out): +@pytest.mark.usefixtures('some_files') +def test_multiline(cap_out): ret = pygrep.main(('--multiline', r'foo\nbar', 'f1', 'f2', 'f3')) out = cap_out.get() assert ret == 1 assert out == 'f1:1:foo\nbar\n' -def test_multiline_line_number(some_files, cap_out): +@pytest.mark.usefixtures('some_files') +def test_multiline_line_number(cap_out): ret = pygrep.main(('--multiline', r'ar', 'f1', 'f2', 'f3')) out = cap_out.get() assert ret == 1 assert out == 'f1:2:bar\n' -def test_multiline_dotall_flag_is_enabled(some_files, cap_out): +@pytest.mark.usefixtures('some_files') +def test_multiline_dotall_flag_is_enabled(cap_out): ret = pygrep.main(('--multiline', r'o.*bar', 'f1', 'f2', 'f3')) out = cap_out.get() assert ret == 1 assert out == 'f1:1:foo\nbar\n' -def test_multiline_multiline_flag_is_enabled(some_files, cap_out): +@pytest.mark.usefixtures('some_files') +def test_multiline_multiline_flag_is_enabled(cap_out): ret = pygrep.main(('--multiline', r'foo$.*bar', 'f1', 'f2', 'f3')) out = cap_out.get() assert ret == 1 From a0658c06bf4cc93ae1af40182c05234c1ba310b2 Mon Sep 17 00:00:00 2001 From: Marco Gorelli Date: Sat, 17 Oct 2020 14:21:12 +0100 Subject: [PATCH 425/967] add --negate flag to pygrep --- pre_commit/languages/pygrep.py | 48 ++++++++++++++++++++++++++--- tests/languages/pygrep_test.py | 55 ++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 40adba0f7..c80d6794b 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -1,6 +1,7 @@ import argparse import re import sys +from typing import NamedTuple from typing import Optional from typing import Pattern from typing import Sequence @@ -45,6 +46,46 @@ def _process_filename_at_once(pattern: Pattern[bytes], filename: str) -> int: return retv +def _process_filename_by_line_negated( + pattern: Pattern[bytes], + filename: str, +) -> int: + with open(filename, 'rb') as f: + for line in f: + if pattern.search(line): + return 0 + else: + output.write_line(filename) + return 1 + + +def _process_filename_at_once_negated( + pattern: Pattern[bytes], + filename: str, +) -> int: + with open(filename, 'rb') as f: + contents = f.read() + match = pattern.search(contents) + if match: + return 0 + else: + output.write_line(filename) + return 1 + + +class Choice(NamedTuple): + multiline: bool + negate: bool + + +FNS = { + Choice(multiline=True, negate=True): _process_filename_at_once_negated, + Choice(multiline=True, negate=False): _process_filename_at_once, + Choice(multiline=False, negate=True): _process_filename_by_line_negated, + Choice(multiline=False, negate=False): _process_filename_by_line, +} + + def run_hook( hook: Hook, file_args: Sequence[str], @@ -64,6 +105,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: ) parser.add_argument('-i', '--ignore-case', action='store_true') parser.add_argument('--multiline', action='store_true') + parser.add_argument('--negate', action='store_true') parser.add_argument('pattern', help='python regex pattern.') parser.add_argument('filenames', nargs='*') args = parser.parse_args(argv) @@ -75,11 +117,9 @@ def main(argv: Optional[Sequence[str]] = None) -> int: pattern = re.compile(args.pattern.encode(), flags) retv = 0 + process_fn = FNS[Choice(multiline=args.multiline, negate=args.negate)] for filename in args.filenames: - if args.multiline: - retv |= _process_filename_at_once(pattern, filename) - else: - retv |= _process_filename_by_line(pattern, filename) + retv |= process_fn(pattern, filename) return retv diff --git a/tests/languages/pygrep_test.py b/tests/languages/pygrep_test.py index 6eef56b7a..d8bacc484 100644 --- a/tests/languages/pygrep_test.py +++ b/tests/languages/pygrep_test.py @@ -8,6 +8,9 @@ def some_files(tmpdir): tmpdir.join('f1').write_binary(b'foo\nbar\n') tmpdir.join('f2').write_binary(b'[INFO] hi\n') tmpdir.join('f3').write_binary(b"with'quotes\n") + tmpdir.join('f4').write_binary(b'foo\npattern\nbar\n') + tmpdir.join('f5').write_binary(b'[INFO] hi\npattern\nbar') + tmpdir.join('f6').write_binary(b"pattern\nbarwith'foo\n") with tmpdir.as_cwd(): yield @@ -30,6 +33,58 @@ def test_main(cap_out, pattern, expected_retcode, expected_out): assert out == expected_out +@pytest.mark.usefixtures('some_files') +def test_negate_by_line_no_match(cap_out): + ret = pygrep.main(('pattern\nbar', 'f4', 'f5', 'f6', '--negate')) + out = cap_out.get() + assert ret == 1 + assert out == 'f4\nf5\nf6\n' + + +@pytest.mark.usefixtures('some_files') +def test_negate_by_line_two_match(cap_out): + ret = pygrep.main(('foo', 'f4', 'f5', 'f6', '--negate')) + out = cap_out.get() + assert ret == 1 + assert out == 'f5\n' + + +@pytest.mark.usefixtures('some_files') +def test_negate_by_line_all_match(cap_out): + ret = pygrep.main(('pattern', 'f4', 'f5', 'f6', '--negate')) + out = cap_out.get() + assert ret == 0 + assert out == '' + + +@pytest.mark.usefixtures('some_files') +def test_negate_by_file_no_match(cap_out): + ret = pygrep.main(('baz', 'f4', 'f5', 'f6', '--negate', '--multiline')) + out = cap_out.get() + assert ret == 1 + assert out == 'f4\nf5\nf6\n' + + +@pytest.mark.usefixtures('some_files') +def test_negate_by_file_one_match(cap_out): + ret = pygrep.main( + ('foo\npattern', 'f4', 'f5', 'f6', '--negate', '--multiline'), + ) + out = cap_out.get() + assert ret == 1 + assert out == 'f5\nf6\n' + + +@pytest.mark.usefixtures('some_files') +def test_negate_by_file_all_match(cap_out): + ret = pygrep.main( + ('pattern\nbar', 'f4', 'f5', 'f6', '--negate', '--multiline'), + ) + out = cap_out.get() + assert ret == 0 + assert out == '' + + @pytest.mark.usefixtures('some_files') def test_ignore_case(cap_out): ret = pygrep.main(('--ignore-case', 'info', 'f1', 'f2', 'f3')) From 653cdd286be27b5c7eca6cae7204753892cabdef Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 26 Oct 2020 16:11:27 -0700 Subject: [PATCH 426/967] Add pre-commit.ci --- README.md | 2 +- azure-pipelines.yml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 98a6d00e0..de7032cb9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![Build Status](https://dev.azure.com/asottile/asottile/_apis/build/status/pre-commit.pre-commit?branchName=master)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master) [![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/21/master.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master) -[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) +[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/pre-commit/pre-commit/master.svg)](https://results.pre-commit.ci/latest/github/pre-commit/pre-commit/master) ## pre-commit diff --git a/azure-pipelines.yml b/azure-pipelines.yml index fb400107d..41f1e5f9b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -13,7 +13,6 @@ resources: ref: refs/tags/v2.0.0 jobs: -- template: job--pre-commit.yml@asottile - template: job--python-tox.yml@asottile parameters: toxenvs: [py37] From 70ab1c3b6f30e8e4e4d25f84b2f12ca2ea843940 Mon Sep 17 00:00:00 2001 From: Joseph Moniz Date: Fri, 9 Oct 2020 13:39:18 -0400 Subject: [PATCH 427/967] add coursier (jvm) as a language --- azure-pipelines.yml | 8 +++ pre_commit/languages/all.py | 2 + pre_commit/languages/coursier.py | 71 +++++++++++++++++++ testing/gen-languages-all | 2 +- testing/get-coursier.ps1 | 11 +++ testing/get-coursier.sh | 13 ++++ .../.pre-commit-channel/echo-java.json | 8 +++ .../.pre-commit-hooks.yaml | 5 ++ testing/util.py | 4 ++ tests/repository_test.py | 10 +++ 10 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 pre_commit/languages/coursier.py create mode 100755 testing/get-coursier.ps1 create mode 100755 testing/get-coursier.sh create mode 100644 testing/resources/coursier_hooks_repo/.pre-commit-channel/echo-java.json create mode 100644 testing/resources/coursier_hooks_repo/.pre-commit-hooks.yaml diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 41f1e5f9b..e7256da18 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -34,6 +34,10 @@ jobs: pre_test: - task: UseRubyVersion@0 - template: step--git-install.yml + - bash: | + testing/get-coursier.sh + echo '##vso[task.prependpath]/tmp/coursier' + displayName: install coursier - bash: | testing/get-swift.sh echo '##vso[task.prependpath]/tmp/swift/usr/bin' @@ -44,6 +48,10 @@ jobs: os: linux pre_test: - task: UseRubyVersion@0 + - bash: | + testing/get-coursier.sh + echo '##vso[task.prependpath]/tmp/coursier' + displayName: install coursier - bash: | testing/get-swift.sh echo '##vso[task.prependpath]/tmp/swift/usr/bin' diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index f32780c14..9c2e59d78 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -6,6 +6,7 @@ from pre_commit.hook import Hook from pre_commit.languages import conda +from pre_commit.languages import coursier from pre_commit.languages import docker from pre_commit.languages import docker_image from pre_commit.languages import dotnet @@ -41,6 +42,7 @@ class Language(NamedTuple): languages = { # BEGIN GENERATED (testing/gen-languages-all) 'conda': Language(name='conda', ENVIRONMENT_DIR=conda.ENVIRONMENT_DIR, get_default_version=conda.get_default_version, healthy=conda.healthy, install_environment=conda.install_environment, run_hook=conda.run_hook), # noqa: E501 + 'coursier': Language(name='coursier', ENVIRONMENT_DIR=coursier.ENVIRONMENT_DIR, get_default_version=coursier.get_default_version, healthy=coursier.healthy, install_environment=coursier.install_environment, run_hook=coursier.run_hook), # noqa: E501 'docker': Language(name='docker', ENVIRONMENT_DIR=docker.ENVIRONMENT_DIR, get_default_version=docker.get_default_version, healthy=docker.healthy, install_environment=docker.install_environment, run_hook=docker.run_hook), # noqa: E501 'docker_image': Language(name='docker_image', ENVIRONMENT_DIR=docker_image.ENVIRONMENT_DIR, get_default_version=docker_image.get_default_version, healthy=docker_image.healthy, install_environment=docker_image.install_environment, run_hook=docker_image.run_hook), # noqa: E501 'dotnet': Language(name='dotnet', ENVIRONMENT_DIR=dotnet.ENVIRONMENT_DIR, get_default_version=dotnet.get_default_version, healthy=dotnet.healthy, install_environment=dotnet.install_environment, run_hook=dotnet.run_hook), # noqa: E501 diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py new file mode 100644 index 000000000..2841467fc --- /dev/null +++ b/pre_commit/languages/coursier.py @@ -0,0 +1,71 @@ +import contextlib +import os +from typing import Generator +from typing import Sequence +from typing import Tuple + +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import Var +from pre_commit.hook import Hook +from pre_commit.languages import helpers +from pre_commit.prefix import Prefix +from pre_commit.util import clean_path_on_failure + +ENVIRONMENT_DIR = 'coursier' + +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy + + +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: # pragma: win32 no cover + helpers.assert_version_default('coursier', version) + helpers.assert_no_additional_deps('coursier', additional_dependencies) + + envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) + channel = prefix.path('.pre-commit-channel') + with clean_path_on_failure(envdir): + for app_descriptor in os.listdir(channel): + _, app_file = os.path.split(app_descriptor) + app, _ = os.path.splitext(app_file) + helpers.run_setup_cmd( + prefix, + ( + 'cs', + 'install', + '--default-channels=false', + f'--channel={channel}', + app, + f'--dir={envdir}', + ), + ) + + +def get_env_patch(target_dir: str) -> PatchesT: # pragma: win32 no cover + return ( + ('PATH', (target_dir, os.pathsep, Var('PATH'))), + ) + + +@contextlib.contextmanager +def in_env( + prefix: Prefix, +) -> Generator[None, None, None]: # pragma: win32 no cover + target_dir = prefix.path( + helpers.environment_dir(ENVIRONMENT_DIR, get_default_version()), + ) + with envcontext(get_env_patch(target_dir)): + yield + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: # pragma: win32 no cover + with in_env(hook.prefix): + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/testing/gen-languages-all b/testing/gen-languages-all index 35eac042b..d9b01bd04 100755 --- a/testing/gen-languages-all +++ b/testing/gen-languages-all @@ -2,7 +2,7 @@ import sys LANGUAGES = [ - 'conda', 'docker', 'dotnet', 'docker_image', 'fail', 'golang', + 'conda', 'coursier', 'docker', 'dotnet', 'docker_image', 'fail', 'golang', 'node', 'perl', 'pygrep', 'python', 'ruby', 'rust', 'script', 'swift', 'system', ] diff --git a/testing/get-coursier.ps1 b/testing/get-coursier.ps1 new file mode 100755 index 000000000..42e563549 --- /dev/null +++ b/testing/get-coursier.ps1 @@ -0,0 +1,11 @@ +$wc = New-Object System.Net.WebClient + +$coursier_url = "https://github.com/coursier/coursier/releases/download/v2.0.5/cs-x86_64-pc-win32.exe" +$coursier_dest = "C:\coursier\cs.exe" +$coursier_hash ="d63d497f7805261e1cd657b8aaa626f6b8f7264cdb68219b2e6be9dd882033a9" + +New-Item -Path "C:\" -Name "coursier" -ItemType "directory" +$wc.DownloadFile($coursier_url, $coursier_dest) +if ((Get-FileHash $coursier_dest -Algorithm SHA256).Hash -ne $coursier_hash) { + throw "Invalid coursier file" +} diff --git a/testing/get-coursier.sh b/testing/get-coursier.sh new file mode 100755 index 000000000..760c6c125 --- /dev/null +++ b/testing/get-coursier.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# This is a script used in CI to install coursier +set -euxo pipefail + +COURSIER_URL="https://github.com/coursier/coursier/releases/download/v2.0.0/cs-x86_64-pc-linux" +COURSIER_HASH="e2e838b75bc71b16bcb77ce951ad65660c89bda7957c79a0628ec7146d35122f" +ARTIFACT="/tmp/coursier/cs" + +mkdir -p /tmp/coursier +rm -f "$ARTIFACT" +curl --location --silent --output "$ARTIFACT" "$COURSIER_URL" +echo "$COURSIER_HASH $ARTIFACT" | sha256sum --check +chmod ugo+x /tmp/coursier/cs diff --git a/testing/resources/coursier_hooks_repo/.pre-commit-channel/echo-java.json b/testing/resources/coursier_hooks_repo/.pre-commit-channel/echo-java.json new file mode 100644 index 000000000..37f401e2c --- /dev/null +++ b/testing/resources/coursier_hooks_repo/.pre-commit-channel/echo-java.json @@ -0,0 +1,8 @@ +{ + "repositories": [ + "central" + ], + "dependencies": [ + "io.get-coursier:echo:latest.stable" + ] +} diff --git a/testing/resources/coursier_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/coursier_hooks_repo/.pre-commit-hooks.yaml new file mode 100644 index 000000000..d4a143b3d --- /dev/null +++ b/testing/resources/coursier_hooks_repo/.pre-commit-hooks.yaml @@ -0,0 +1,5 @@ +- id: echo-java + name: echo-java + description: echo from java + entry: echo-java + language: coursier diff --git a/testing/util.py b/testing/util.py index f556a8dd9..18cd73427 100644 --- a/testing/util.py +++ b/testing/util.py @@ -40,6 +40,10 @@ def cmd_output_mocked_pre_commit_home( return ret, out.replace('\r\n', '\n'), None +skipif_cant_run_coursier = pytest.mark.skipif( + os.name == 'nt' or parse_shebang.find_executable('cs') is None, + reason="coursier isn't installed or can't be found", +) skipif_cant_run_docker = pytest.mark.skipif( os.name == 'nt' or not docker_is_running(), reason="Docker isn't running or can't be accessed", diff --git a/tests/repository_test.py b/tests/repository_test.py index a6d801ec1..3d5093df6 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -31,6 +31,7 @@ from testing.fixtures import modify_manifest from testing.util import cwd from testing.util import get_resource_path +from testing.util import skipif_cant_run_coursier from testing.util import skipif_cant_run_docker from testing.util import skipif_cant_run_swift from testing.util import xfailif_windows @@ -195,6 +196,15 @@ def test_versioned_python_hook(tempdir_factory, store): ) +@skipif_cant_run_coursier # pragma: win32 no cover +def test_run_a_coursier_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'coursier_hooks_repo', + 'echo-java', + ['Hello World from coursier'], b'Hello World from coursier\n', + ) + + @skipif_cant_run_docker # pragma: win32 no cover def test_run_a_docker_hook(tempdir_factory, store): _test_hook_repo( From 47e758d8f1e711cbc53b8fe7f2629b09b3aa72c4 Mon Sep 17 00:00:00 2001 From: int3l Date: Thu, 17 Sep 2020 00:45:15 +0300 Subject: [PATCH 428/967] Distinct error handling exit codes https://tldp.org/LDP/abs/html/exitcodes.html - exit codes convention --- pre_commit/error_handler.py | 17 +++++++++++------ tests/error_handler_test.py | 16 ++++++++++------ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index afacab9bb..023dd3596 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -12,7 +12,12 @@ from pre_commit.util import force_bytes -def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None: +def _log_and_exit( + msg: str, + ret_code: int, + exc: BaseException, + formatted: str, +) -> None: error_msg = f'{msg}: {type(exc).__name__}: '.encode() + force_bytes(exc) output.write_line_b(error_msg) @@ -51,7 +56,7 @@ def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None: _log_line('```') _log_line(formatted.rstrip()) _log_line('```') - raise SystemExit(1) + raise SystemExit(ret_code) @contextlib.contextmanager @@ -60,9 +65,9 @@ def error_handler() -> Generator[None, None, None]: yield except (Exception, KeyboardInterrupt) as e: if isinstance(e, FatalError): - msg = 'An error has occurred' + msg, ret_code = 'An error has occurred', 1 elif isinstance(e, KeyboardInterrupt): - msg = 'Interrupted (^C)' + msg, ret_code = 'Interrupted (^C)', 130 else: - msg = 'An unexpected error has occurred' - _log_and_exit(msg, e, traceback.format_exc()) + msg, ret_code = 'An unexpected error has occurred', 3 + _log_and_exit(msg, ret_code, e, traceback.format_exc()) diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 804701f05..6b0bb86d7 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -33,6 +33,7 @@ def test_error_handler_fatal_error(mocked_log_and_exit): mocked_log_and_exit.assert_called_once_with( 'An error has occurred', + 1, exc, # Tested below mock.ANY, @@ -47,7 +48,7 @@ def test_error_handler_fatal_error(mocked_log_and_exit): r' raise exc\n' r'(pre_commit\.errors\.)?FatalError: just a test\n', ) - pattern.assert_matches(mocked_log_and_exit.call_args[0][2]) + pattern.assert_matches(mocked_log_and_exit.call_args[0][3]) def test_error_handler_uncaught_error(mocked_log_and_exit): @@ -57,6 +58,7 @@ def test_error_handler_uncaught_error(mocked_log_and_exit): mocked_log_and_exit.assert_called_once_with( 'An unexpected error has occurred', + 3, exc, # Tested below mock.ANY, @@ -70,7 +72,7 @@ def test_error_handler_uncaught_error(mocked_log_and_exit): r' raise exc\n' r'ValueError: another test\n', ) - pattern.assert_matches(mocked_log_and_exit.call_args[0][2]) + pattern.assert_matches(mocked_log_and_exit.call_args[0][3]) def test_error_handler_keyboardinterrupt(mocked_log_and_exit): @@ -80,6 +82,7 @@ def test_error_handler_keyboardinterrupt(mocked_log_and_exit): mocked_log_and_exit.assert_called_once_with( 'Interrupted (^C)', + 130, exc, # Tested below mock.ANY, @@ -93,7 +96,7 @@ def test_error_handler_keyboardinterrupt(mocked_log_and_exit): r' raise exc\n' r'KeyboardInterrupt\n', ) - pattern.assert_matches(mocked_log_and_exit.call_args[0][2]) + pattern.assert_matches(mocked_log_and_exit.call_args[0][3]) def test_log_and_exit(cap_out, mock_store_dir): @@ -103,8 +106,9 @@ def test_log_and_exit(cap_out, mock_store_dir): 'pre_commit.errors.FatalError: hai\n' ) - with pytest.raises(SystemExit): - error_handler._log_and_exit('msg', FatalError('hai'), tb) + with pytest.raises(SystemExit) as excinfo: + error_handler._log_and_exit('msg', 1, FatalError('hai'), tb) + assert excinfo.value.code == 1 printed = cap_out.get() log_file = os.path.join(mock_store_dir, 'pre-commit.log') @@ -170,7 +174,7 @@ def test_error_handler_no_tty(tempdir_factory): 'from pre_commit.error_handler import error_handler\n' 'with error_handler():\n' ' raise ValueError("\\u2603")\n', - retcode=1, + retcode=3, tempdir_factory=tempdir_factory, pre_commit_home=pre_commit_home, ) From 24dfeed89c0d4d34c46e3310cf28918747d766e1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 28 Oct 2020 13:00:25 -0700 Subject: [PATCH 429/967] Replace EnvironT with MutableMapping[str, str] --- pre_commit/commands/run.py | 8 ++++---- pre_commit/envcontext.py | 9 ++++----- pre_commit/git.py | 6 ++++-- pre_commit/util.py | 3 --- pre_commit/xargs.py | 4 ++-- tests/commands/run_test.py | 6 +++--- 6 files changed, 17 insertions(+), 19 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 1f28c8c74..0d335e285 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -11,6 +11,7 @@ from typing import Collection from typing import Dict from typing import List +from typing import MutableMapping from typing import Sequence from typing import Set from typing import Tuple @@ -28,7 +29,6 @@ from pre_commit.staged_files_only import staged_files_only from pre_commit.store import Store from pre_commit.util import cmd_output_b -from pre_commit.util import EnvironT logger = logging.getLogger('pre_commit') @@ -116,7 +116,7 @@ def from_config( return Classifier(filenames) -def _get_skips(environ: EnvironT) -> Set[str]: +def _get_skips(environ: MutableMapping[str, str]) -> Set[str]: skips = environ.get('SKIP', '') return {skip.strip() for skip in skips.split(',') if skip.strip()} @@ -258,7 +258,7 @@ def _run_hooks( config: Dict[str, Any], hooks: Sequence[Hook], args: argparse.Namespace, - environ: EnvironT, + environ: MutableMapping[str, str], ) -> int: """Actually run the hooks.""" skips = _get_skips(environ) @@ -315,7 +315,7 @@ def run( config_file: str, store: Store, args: argparse.Namespace, - environ: EnvironT = os.environ, + environ: MutableMapping[str, str] = os.environ, ) -> int: stash = not args.all_files and not args.files diff --git a/pre_commit/envcontext.py b/pre_commit/envcontext.py index 16d3d15e3..4ab0d8cb9 100644 --- a/pre_commit/envcontext.py +++ b/pre_commit/envcontext.py @@ -2,13 +2,12 @@ import enum import os from typing import Generator +from typing import MutableMapping from typing import NamedTuple from typing import Optional from typing import Tuple from typing import Union -from pre_commit.util import EnvironT - class _Unset(enum.Enum): UNSET = 1 @@ -27,7 +26,7 @@ class Var(NamedTuple): PatchesT = Tuple[Tuple[str, ValueT], ...] -def format_env(parts: SubstitutionT, env: EnvironT) -> str: +def format_env(parts: SubstitutionT, env: MutableMapping[str, str]) -> str: return ''.join( env.get(part.name, part.default) if isinstance(part, Var) else part for part in parts @@ -37,7 +36,7 @@ def format_env(parts: SubstitutionT, env: EnvironT) -> str: @contextlib.contextmanager def envcontext( patch: PatchesT, - _env: Optional[EnvironT] = None, + _env: Optional[MutableMapping[str, str]] = None, ) -> Generator[None, None, None]: """In this context, `os.environ` is modified according to `patch`. @@ -50,7 +49,7 @@ def envcontext( replaced with the previous environment """ env = os.environ if _env is None else _env - before = env.copy() + before = dict(env) for k, v in patch: if v is UNSET: diff --git a/pre_commit/git.py b/pre_commit/git.py index ca30eaa7e..13ba664c8 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -3,6 +3,7 @@ import sys from typing import Dict from typing import List +from typing import MutableMapping from typing import Optional from typing import Set @@ -10,7 +11,6 @@ from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b -from pre_commit.util import EnvironT logger = logging.getLogger(__name__) @@ -24,7 +24,9 @@ def zsplit(s: str) -> List[str]: return [] -def no_git_env(_env: Optional[EnvironT] = None) -> Dict[str, str]: +def no_git_env( + _env: Optional[MutableMapping[str, str]] = None, +) -> Dict[str, str]: # Too many bugs dealing with environment variables and GIT: # https://github.com/pre-commit/pre-commit/issues/300 # In git 2.6.3 (maybe others), git exports GIT_WORK_TREE while running diff --git a/pre_commit/util.py b/pre_commit/util.py index 0338b3737..f4cf7045a 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -16,7 +16,6 @@ from typing import Optional from typing import Tuple from typing import Type -from typing import Union import yaml @@ -29,8 +28,6 @@ from importlib_resources import open_binary from importlib_resources import read_text -EnvironT = Union[Dict[str, str], 'os._Environ'] - Loader = getattr(yaml, 'CSafeLoader', yaml.SafeLoader) yaml_load = functools.partial(yaml.load, Loader=Loader) Dumper = getattr(yaml, 'CSafeDumper', yaml.SafeDumper) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 5235dc650..7538b54f6 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -9,6 +9,7 @@ from typing import Generator from typing import Iterable from typing import List +from typing import MutableMapping from typing import Optional from typing import Sequence from typing import Tuple @@ -17,13 +18,12 @@ from pre_commit import parse_shebang from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_p -from pre_commit.util import EnvironT TArg = TypeVar('TArg') TRet = TypeVar('TRet') -def _environ_size(_env: Optional[EnvironT] = None) -> int: +def _environ_size(_env: Optional[MutableMapping[str, str]] = None) -> int: environ = _env if _env is not None else getattr(os, 'environb', os.environ) size = 8 * len(environ) # number of pointers in `envp` for k, v in environ.items(): diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 2461ed5b3..00b471282 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -2,6 +2,7 @@ import shlex import sys import time +from typing import MutableMapping from unittest import mock import pytest @@ -18,7 +19,6 @@ from pre_commit.commands.run import filter_by_include_exclude from pre_commit.commands.run import run from pre_commit.util import cmd_output -from pre_commit.util import EnvironT from pre_commit.util import make_executable from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import add_config_to_repo @@ -482,7 +482,7 @@ def test_all_push_options_ok(cap_out, store, repo_with_passing_hook): def test_checkout_type(cap_out, store, repo_with_passing_hook): args = run_opts(from_ref='', to_ref='', checkout_type='1') - environ: EnvironT = {} + environ: MutableMapping[str, str] = {} ret, printed = _do_run( cap_out, store, repo_with_passing_hook, args, environ, ) @@ -1032,7 +1032,7 @@ def test_skipped_without_any_setup_for_post_checkout(in_git_dir, store): def test_pre_commit_env_variable_set(cap_out, store, repo_with_passing_hook): args = run_opts() - environ: EnvironT = {} + environ: MutableMapping[str, str] = {} ret, printed = _do_run( cap_out, store, repo_with_passing_hook, args, environ, ) From 29f3e67655f6bf76de402226fcd058966fd24cdd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 28 Oct 2020 13:56:26 -0700 Subject: [PATCH 430/967] improve node install by using npm pack --- pre_commit/languages/node.py | 21 +++++++++++++++++---- tests/languages/node_test.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index dccbb7ca2..59e534068 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -19,6 +19,7 @@ from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b +from pre_commit.util import rmtree ENVIRONMENT_DIR = 'node_env' @@ -99,11 +100,23 @@ def install_environment( with in_env(prefix, version): # https://npm.community/t/npm-install-g-git-vs-git-clone-cd-npm-install-g/5449 # install as if we installed from git - helpers.run_setup_cmd(prefix, ('npm', 'install')) - helpers.run_setup_cmd( - prefix, - ('npm', 'install', '-g', '.', *additional_dependencies), + + local_install_cmd = ( + 'npm', 'install', '--dev', '--prod', + '--ignore-prepublish', '--no-progress', '--no-save', ) + helpers.run_setup_cmd(prefix, local_install_cmd) + + _, pkg, _ = cmd_output('npm', 'pack', cwd=prefix.prefix_dir) + pkg = prefix.path(pkg.strip()) + + install = ('npm', 'install', '-g', pkg, *additional_dependencies) + helpers.run_setup_cmd(prefix, install) + + # clean these up after installation + if prefix.exists('node_modules'): # pragma: win32 no cover + rmtree(prefix.path('node_modules')) + os.remove(pkg) def run_hook( diff --git a/tests/languages/node_test.py b/tests/languages/node_test.py index c8e2d47d1..8e52268ff 100644 --- a/tests/languages/node_test.py +++ b/tests/languages/node_test.py @@ -1,3 +1,4 @@ +import json import os import shutil import sys @@ -10,6 +11,7 @@ from pre_commit import parse_shebang from pre_commit.languages import node from pre_commit.prefix import Prefix +from pre_commit.util import cmd_output from testing.util import xfailif_windows @@ -78,3 +80,29 @@ def test_unhealthy_if_system_node_goes_missing(tmpdir): node_bin.remove() assert not node.healthy(prefix, 'system') + + +@xfailif_windows # pragma: win32 no cover +def test_installs_without_links_outside_env(tmpdir): + tmpdir.join('bin/main.js').ensure().write( + '#!/usr/bin/env node\n' + '_ = require("lodash"); console.log("success!")\n', + ) + tmpdir.join('package.json').write( + json.dumps({ + 'name': 'foo', + 'version': '0.0.1', + 'bin': {'foo': './bin/main.js'}, + 'dependencies': {'lodash': '*'}, + }), + ) + + prefix = Prefix(str(tmpdir)) + node.install_environment(prefix, 'system', ()) + assert node.healthy(prefix, 'system') + + # this directory shouldn't exist, make sure we succeed without it existing + cmd_output('rm', '-rf', str(tmpdir.join('node_modules'))) + + with node.in_env(prefix, 'system'): + assert cmd_output('foo')[1] == 'success!\n' From 7f9f66e542395ba743e243bdcd92df4e5500d57d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 28 Oct 2020 14:54:52 -0700 Subject: [PATCH 431/967] don't use system for ruby/node if it is a shim exe --- pre_commit/languages/helpers.py | 21 +++++++++++++++ pre_commit/languages/node.py | 3 +-- pre_commit/languages/ruby.py | 3 +-- tests/languages/helpers_test.py | 45 ++++++++++++++++++++++++++++++++- 4 files changed, 67 insertions(+), 5 deletions(-) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 01c65ab69..69e127878 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -1,6 +1,7 @@ import multiprocessing import os import random +import re from typing import Any from typing import List from typing import Optional @@ -10,6 +11,7 @@ from typing import TYPE_CHECKING import pre_commit.constants as C +from pre_commit import parse_shebang from pre_commit.hook import Hook from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b @@ -20,6 +22,25 @@ FIXED_RANDOM_SEED = 1542676187 +SHIMS_RE = re.compile(r'[/\\]shims[/\\]') + + +def exe_exists(exe: str) -> bool: + found = parse_shebang.find_executable(exe) + if found is None: # exe exists + return False + + homedir = os.path.expanduser('~') + try: + common: Optional[str] = os.path.commonpath((found, homedir)) + except ValueError: # on windows, different drives raises ValueError + common = None + + return ( + not SHIMS_RE.search(found) and # it is not in a /shims/ directory + common != homedir # it is not in the home directory + ) + def run_setup_cmd(prefix: Prefix, cmd: Tuple[str, ...]) -> None: cmd_output_b(*cmd, cwd=prefix.prefix_dir) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 59e534068..8dc4e8ba9 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -7,7 +7,6 @@ from typing import Tuple import pre_commit.constants as C -from pre_commit import parse_shebang from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET @@ -31,7 +30,7 @@ def get_default_version() -> str: return C.DEFAULT # if node is already installed, we can save a bunch of setup time by # using the installed version - elif all(parse_shebang.find_executable(exe) for exe in ('node', 'npm')): + elif all(helpers.exe_exists(exe) for exe in ('node', 'npm')): return 'system' else: return C.DEFAULT diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index ef73961f1..b6c0bd79f 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -8,7 +8,6 @@ from typing import Tuple import pre_commit.constants as C -from pre_commit import parse_shebang from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET @@ -26,7 +25,7 @@ @functools.lru_cache(maxsize=1) def get_default_version() -> str: - if all(parse_shebang.find_executable(exe) for exe in ('ruby', 'gem')): + if all(helpers.exe_exists(exe) for exe in ('ruby', 'gem')): return 'system' else: return C.DEFAULT diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index fa493cc04..2e8277e02 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -1,17 +1,60 @@ import multiprocessing -import os +import os.path import sys from unittest import mock import pytest import pre_commit.constants as C +from pre_commit import parse_shebang from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError from testing.auto_namedtuple import auto_namedtuple +@pytest.fixture +def find_exe_mck(): + with mock.patch.object(parse_shebang, 'find_executable') as mck: + yield mck + + +@pytest.fixture +def homedir_mck(): + def fake_expanduser(pth): + assert pth == '~' + return os.path.normpath('/home/me') + + with mock.patch.object(os.path, 'expanduser', fake_expanduser): + yield + + +def test_exe_exists_does_not_exist(find_exe_mck, homedir_mck): + find_exe_mck.return_value = None + assert helpers.exe_exists('ruby') is False + + +def test_exe_exists_exists(find_exe_mck, homedir_mck): + find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby') + assert helpers.exe_exists('ruby') is True + + +def test_exe_exists_false_if_shim(find_exe_mck, homedir_mck): + find_exe_mck.return_value = os.path.normpath('/foo/shims/ruby') + assert helpers.exe_exists('ruby') is False + + +def test_exe_exists_false_if_homedir(find_exe_mck, homedir_mck): + find_exe_mck.return_value = os.path.normpath('/home/me/somedir/ruby') + assert helpers.exe_exists('ruby') is False + + +def test_exe_exists_commonpath_raises_ValueError(find_exe_mck, homedir_mck): + find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby') + with mock.patch.object(os.path, 'commonpath', side_effect=ValueError): + assert helpers.exe_exists('ruby') is True + + def test_basic_get_default_version(): assert helpers.basic_get_default_version() == C.DEFAULT From a3c9721d8f4df3de7104f61b336dea3feb5fa52c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 28 Oct 2020 21:59:03 -0700 Subject: [PATCH 432/967] v2.8.0 --- .pre-commit-config.yaml | 18 +++++++------- CHANGELOG.md | 54 +++++++++++++++++++++++++++++++++++++++++ setup.cfg | 3 ++- 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e9cf73946..80fa14bbe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.1.0 + rev: v3.3.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -12,25 +12,25 @@ repos: - id: requirements-txt-fixer - id: double-quote-string-fixer - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.3 + rev: 3.8.4 hooks: - id: flake8 - additional_dependencies: [flake8-typing-imports==1.6.0] + additional_dependencies: [flake8-typing-imports==1.10.0] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.5.3 + rev: v1.5.4 hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.6.0 + rev: v2.7.1 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.6.2 + rev: v2.7.3 hooks: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/asottile/reorder_python_imports - rev: v2.3.0 + rev: v2.3.5 hooks: - id: reorder-python-imports args: [--py3-plus] @@ -40,11 +40,11 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.10.0 + rev: v1.15.1 hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.782 + rev: v0.790 hooks: - id: mypy exclude: ^testing/resources/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 1621bb3fc..a56701e3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,57 @@ +2.8.0 - 2020-10-28 +================== + +### Fixes +- Improve `healthy()` check for `language: node` + `language_version: system` + hooks when the system executable goes missing. + - pre-commit/action#45 issue by @KOliver94. + - #1589 issue by @asottile. + - #1590 PR by @asottile. +- Fix excess whitespace in error log traceback + - #1592 PR by @asottile. +- Fix posixlike shebang invocations with shim executables of the git hook + script on windows. + - #1593 issue by @Celeborn2BeAlive. + - #1595 PR by @Celeborn2BeAlive. +- Remove hard-coded `C:\PythonXX\python.exe` path on windows as it caused + confusion (and `virtualenv` can sometimes do better) + - #1599 PR by @asottile. +- Fix `language: ruby` hooks when `--format-executable` is present in a gemrc + - issue by `Rainbow Tux` (discord). + - #1603 PR by @asottile. +- Move `cygwin` / `win32` mismatch error earlier to catch msys2 mismatches + - #1605 issue by @danyeaw. + - #1606 PR by @asottile. +- Remove `-p` workaround for old `virtualenv` + - #1617 PR by @asottile. +- Fix `language: node` installations to not symlink outside of the environment + - pre-commit-ci/issues#2 issue by @DanielJSottile. + - #1667 PR by @asottile. +- Don't identify shim executables as valid `system` for defaulting + `language_version` for `language: node` / `language: ruby` + - #1658 issue by @adithyabsk. + - #1668 PR by @asottile. + +### Features +- Update `rbenv` / `ruby-build` + - #1612 issue by @tdeo. + - #1614 PR by @asottile. +- Update `sample-config` versions + - #1611 PR by @mcsitter. +- Add new language: `dotnet` + - #1598 by @rkm. +- Add `--negate` option to `language: pygrep` hooks + - #1643 PR by @MarcoGorelli. +- Add zipapp support + - #1616 PR by @asottile. +- Run pre-commit through https://pre-commit.ci + - #1662 PR by @asottile. +- Add new language: `coursier` (a jvm-based package manager) + - #1633 PR by @JosephMoniz. +- Exit with distinct codes: 1 (user error), 3 (unexpected error), 130 (^C) + - #1601 PR by @int3l. + + 2.7.1 - 2020-08-23 ================== diff --git a/setup.cfg b/setup.cfg index 4153d7650..eb7a8e199 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.7.1 +version = 2.8.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown @@ -16,6 +16,7 @@ classifiers = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy From 711248f6785e60d698d915e79841df65de40634a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 28 Oct 2020 22:01:15 -0700 Subject: [PATCH 433/967] show features first --- CHANGELOG.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a56701e3b..0ae2d7452 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,25 @@ 2.8.0 - 2020-10-28 ================== +### Features +- Update `rbenv` / `ruby-build` + - #1612 issue by @tdeo. + - #1614 PR by @asottile. +- Update `sample-config` versions + - #1611 PR by @mcsitter. +- Add new language: `dotnet` + - #1598 by @rkm. +- Add `--negate` option to `language: pygrep` hooks + - #1643 PR by @MarcoGorelli. +- Add zipapp support + - #1616 PR by @asottile. +- Run pre-commit through https://pre-commit.ci + - #1662 PR by @asottile. +- Add new language: `coursier` (a jvm-based package manager) + - #1633 PR by @JosephMoniz. +- Exit with distinct codes: 1 (user error), 3 (unexpected error), 130 (^C) + - #1601 PR by @int3l. + ### Fixes - Improve `healthy()` check for `language: node` + `language_version: system` hooks when the system executable goes missing. @@ -32,25 +51,6 @@ - #1658 issue by @adithyabsk. - #1668 PR by @asottile. -### Features -- Update `rbenv` / `ruby-build` - - #1612 issue by @tdeo. - - #1614 PR by @asottile. -- Update `sample-config` versions - - #1611 PR by @mcsitter. -- Add new language: `dotnet` - - #1598 by @rkm. -- Add `--negate` option to `language: pygrep` hooks - - #1643 PR by @MarcoGorelli. -- Add zipapp support - - #1616 PR by @asottile. -- Run pre-commit through https://pre-commit.ci - - #1662 PR by @asottile. -- Add new language: `coursier` (a jvm-based package manager) - - #1633 PR by @JosephMoniz. -- Exit with distinct codes: 1 (user error), 3 (unexpected error), 130 (^C) - - #1601 PR by @int3l. - 2.7.1 - 2020-08-23 ================== From 62b8d0ed825bde729a32ffadf0b45f2ea82315f8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 28 Oct 2020 22:56:10 -0700 Subject: [PATCH 434/967] allow default language_version of system when homedir is / --- pre_commit/languages/helpers.py | 10 ++++++++-- tests/languages/helpers_test.py | 6 ++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 69e127878..29138fd1a 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -37,8 +37,14 @@ def exe_exists(exe: str) -> bool: common = None return ( - not SHIMS_RE.search(found) and # it is not in a /shims/ directory - common != homedir # it is not in the home directory + # it is not in a /shims/ directory + not SHIMS_RE.search(found) and + ( + # the homedir is / (docker, service user, etc.) + os.path.dirname(homedir) == homedir or + # the exe is not contained in the home directory + common != homedir + ) ) diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index 2e8277e02..669cd3343 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -55,6 +55,12 @@ def test_exe_exists_commonpath_raises_ValueError(find_exe_mck, homedir_mck): assert helpers.exe_exists('ruby') is True +def test_exe_exists_true_when_homedir_is_slash(find_exe_mck): + find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby') + with mock.patch.object(os.path, 'expanduser', return_value=os.sep): + assert helpers.exe_exists('ruby') is True + + def test_basic_get_default_version(): assert helpers.basic_get_default_version() == C.DEFAULT From b2207e5b044374d90cc349e136279f27e615d0fc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 28 Oct 2020 23:04:31 -0700 Subject: [PATCH 435/967] v2.8.1 --- CHANGELOG.md | 7 +++++++ setup.cfg | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ae2d7452..c26eb8af8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +2.8.1 - 2020-10-28 +================== + +### Fixes +- Allow default `language_version` of `system` when the homedir is `/` + - #1669 PR by @asottile. + 2.8.0 - 2020-10-28 ================== diff --git a/setup.cfg b/setup.cfg index eb7a8e199..94f14ad45 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.8.0 +version = 2.8.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From e05ac1e91fcfa695405df1c18d4432c12e5d7142 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 29 Oct 2020 19:45:06 -0700 Subject: [PATCH 436/967] don't call ruby install for language_version = default --- pre_commit/languages/ruby.py | 4 ++-- tests/languages/ruby_test.py | 40 ++++++++++++++++++++++++++++-------- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index b6c0bd79f..1a0f0c7e3 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -121,8 +121,8 @@ def install_environment( # Need to call this before installing so rbenv's directories # are set up helpers.run_setup_cmd(prefix, ('rbenv', 'init', '-')) - # XXX: this will *always* fail if `version == C.DEFAULT` - _install_ruby(prefix, version) + if version != C.DEFAULT: + _install_ruby(prefix, version) # Need to call this after installing to set up the shims helpers.run_setup_cmd(prefix, ('rbenv', 'rehash')) diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 853bb7321..6c0c9e5e0 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -30,23 +30,45 @@ def test_uses_system_if_both_gem_and_ruby_are_available(find_exe_mck): assert ACTUAL_GET_DEFAULT_VERSION() == 'system' +@pytest.fixture +def fake_gem_prefix(tmpdir): + gemspec = '''\ +Gem::Specification.new do |s| + s.name = 'pre_commit_dummy_package' + s.version = '0.0.0' + s.summary = 'dummy gem for pre-commit hooks' + s.authors = ['Anthony Sottile'] +end +''' + tmpdir.join('dummy_gem.gemspec').write(gemspec) + yield Prefix(tmpdir) + + +@xfailif_windows # pragma: win32 no cover +def test_install_ruby_system(fake_gem_prefix): + ruby.install_environment(fake_gem_prefix, 'system', ()) + + # Should be able to activate and use rbenv install + with ruby.in_env(fake_gem_prefix, 'system'): + _, out, _ = cmd_output('gem', 'list') + assert 'pre_commit_dummy_package' in out + + @xfailif_windows # pragma: win32 no cover -def test_install_rbenv(tempdir_factory): - prefix = Prefix(tempdir_factory.get()) - ruby._install_rbenv(prefix, C.DEFAULT) +def test_install_ruby_default(fake_gem_prefix): + ruby.install_environment(fake_gem_prefix, C.DEFAULT, ()) # Should have created rbenv directory - assert os.path.exists(prefix.path('rbenv-default')) + assert os.path.exists(fake_gem_prefix.path('rbenv-default')) # Should be able to activate using our script and access rbenv - with ruby.in_env(prefix, 'default'): + with ruby.in_env(fake_gem_prefix, 'default'): cmd_output('rbenv', '--help') @xfailif_windows # pragma: win32 no cover -def test_install_rbenv_with_version(tempdir_factory): - prefix = Prefix(tempdir_factory.get()) - ruby._install_rbenv(prefix, version='1.9.3p547') +def test_install_ruby_with_version(fake_gem_prefix): + ruby.install_environment(fake_gem_prefix, '2.7.2', ()) # Should be able to activate and use rbenv install - with ruby.in_env(prefix, '1.9.3p547'): + with ruby.in_env(fake_gem_prefix, '2.7.2'): cmd_output('rbenv', 'install', '--help') From 3112e080883c4973262569d81b6d3307db08b210 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 30 Oct 2020 13:36:35 -0700 Subject: [PATCH 437/967] v2.8.2 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c26eb8af8..ff1013f82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +2.8.2 - 2020-10-30 +================== + +### Fixes +- Fix installation of ruby hooks with `language_version: default` + - #1671 issue by @aerickson. + - #1672 PR by @asottile. + 2.8.1 - 2020-10-28 ================== diff --git a/setup.cfg b/setup.cfg index 94f14ad45..32160b9ea 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.8.1 +version = 2.8.2 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 62f668fc3fdf0180a1d66917a599729395f33e44 Mon Sep 17 00:00:00 2001 From: Marco Gorelli Date: Mon, 2 Nov 2020 15:35:42 +0000 Subject: [PATCH 438/967] add types_or --- pre_commit/clientlib.py | 1 + pre_commit/commands/run.py | 14 +++++++++++--- pre_commit/hook.py | 1 + pre_commit/meta_hooks/check_useless_excludes.py | 6 ++++-- testing/resources/exclude_types_repo/bin/hook.sh | 2 +- testing/resources/failing_hook_repo/bin/hook.sh | 2 +- .../modified_file_returns_zero_repo/bin/hook2.sh | 2 +- testing/resources/script_hooks_repo/bin/hook.sh | 2 +- .../resources/types_or_repo/.pre-commit-hooks.yaml | 6 ++++++ testing/resources/types_or_repo/bin/hook.sh | 3 +++ testing/resources/types_repo/bin/hook.sh | 2 +- tests/commands/run_test.py | 13 +++++++++++++ tests/repository_test.py | 1 + 13 files changed, 45 insertions(+), 10 deletions(-) create mode 100644 testing/resources/types_or_repo/.pre-commit-hooks.yaml create mode 100755 testing/resources/types_or_repo/bin/hook.sh diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 87679bfa6..0b8582bce 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -61,6 +61,7 @@ def _make_argparser(filenames_help: str) -> argparse.ArgumentParser: cfgv.Optional('files', check_string_regex, ''), cfgv.Optional('exclude', check_string_regex, '^$'), cfgv.Optional('types', cfgv.check_array(check_type_tag), ['file']), + cfgv.Optional('types_or', cfgv.check_array(check_type_tag), ['file']), cfgv.Optional('exclude_types', cfgv.check_array(check_type_tag), []), cfgv.Optional( diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 0d335e285..56450e384 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -83,20 +83,28 @@ def by_types( self, names: Sequence[str], types: Collection[str], + types_or: Collection[str], exclude_types: Collection[str], ) -> List[str]: - types, exclude_types = frozenset(types), frozenset(exclude_types) + types = frozenset(types) + types_or = frozenset(types_or) + exclude_types = frozenset(exclude_types) ret = [] for filename in names: tags = self._types_for_file(filename) - if tags >= types and not tags & exclude_types: + if tags >= types and tags & types_or and not tags & exclude_types: ret.append(filename) return ret def filenames_for_hook(self, hook: Hook) -> Tuple[str, ...]: names = self.filenames names = filter_by_include_exclude(names, hook.files, hook.exclude) - names = self.by_types(names, hook.types, hook.exclude_types) + names = self.by_types( + names, + hook.types, + hook.types_or, + hook.exclude_types, + ) return tuple(names) @classmethod diff --git a/pre_commit/hook.py b/pre_commit/hook.py index b65ac42b0..ea773942b 100644 --- a/pre_commit/hook.py +++ b/pre_commit/hook.py @@ -22,6 +22,7 @@ class Hook(NamedTuple): files: str exclude: str types: Sequence[str] + types_or: Sequence[str] exclude_types: Sequence[str] additional_dependencies: Sequence[str] args: Sequence[str] diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index db6865c6c..12be03f8a 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -47,8 +47,10 @@ def check_useless_excludes(config_file: str) -> int: # the defaults applied during runtime hook = apply_defaults(hook, MANIFEST_HOOK_DICT) names = classifier.filenames - types, exclude_types = hook['types'], hook['exclude_types'] - names = classifier.by_types(names, types, exclude_types) + types = hook['types'] + types_or = hook['types_or'] + exclude_types = hook['exclude_types'] + names = classifier.by_types(names, types, types_or, exclude_types) include, exclude = hook['files'], hook['exclude'] if not exclude_matches_any(names, include, exclude): print( diff --git a/testing/resources/exclude_types_repo/bin/hook.sh b/testing/resources/exclude_types_repo/bin/hook.sh index bdade5132..a828db4d2 100755 --- a/testing/resources/exclude_types_repo/bin/hook.sh +++ b/testing/resources/exclude_types_repo/bin/hook.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash -echo $@ +echo "$@" exit 1 diff --git a/testing/resources/failing_hook_repo/bin/hook.sh b/testing/resources/failing_hook_repo/bin/hook.sh index 229ccaf41..7dcffebe8 100755 --- a/testing/resources/failing_hook_repo/bin/hook.sh +++ b/testing/resources/failing_hook_repo/bin/hook.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash echo 'Fail' -echo $@ +echo "$@" exit 1 diff --git a/testing/resources/modified_file_returns_zero_repo/bin/hook2.sh b/testing/resources/modified_file_returns_zero_repo/bin/hook2.sh index 5af177a83..a9f1dcd91 100755 --- a/testing/resources/modified_file_returns_zero_repo/bin/hook2.sh +++ b/testing/resources/modified_file_returns_zero_repo/bin/hook2.sh @@ -1,2 +1,2 @@ #!/usr/bin/env bash -echo $@ +echo "$@" diff --git a/testing/resources/script_hooks_repo/bin/hook.sh b/testing/resources/script_hooks_repo/bin/hook.sh index 6565ee40a..cbc4b3544 100755 --- a/testing/resources/script_hooks_repo/bin/hook.sh +++ b/testing/resources/script_hooks_repo/bin/hook.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash -echo $@ +echo "$@" echo 'Hello World' diff --git a/testing/resources/types_or_repo/.pre-commit-hooks.yaml b/testing/resources/types_or_repo/.pre-commit-hooks.yaml new file mode 100644 index 000000000..a4ea920d6 --- /dev/null +++ b/testing/resources/types_or_repo/.pre-commit-hooks.yaml @@ -0,0 +1,6 @@ +- id: python-cython-files + name: Python and Cython files + entry: bin/hook.sh + language: script + types: [file] + types_or: [python, cython] diff --git a/testing/resources/types_or_repo/bin/hook.sh b/testing/resources/types_or_repo/bin/hook.sh new file mode 100755 index 000000000..a828db4d2 --- /dev/null +++ b/testing/resources/types_or_repo/bin/hook.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +echo "$@" +exit 1 diff --git a/testing/resources/types_repo/bin/hook.sh b/testing/resources/types_repo/bin/hook.sh index bdade5132..a828db4d2 100755 --- a/testing/resources/types_repo/bin/hook.sh +++ b/testing/resources/types_repo/bin/hook.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash -echo $@ +echo "$@" exit 1 diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 00b471282..34f3a3753 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -219,6 +219,19 @@ def test_types_hook_repository(cap_out, store, tempdir_factory): assert b'bar.notpy' not in printed +def test_types_or_hook_repository(cap_out, store, tempdir_factory): + git_path = make_consuming_repo(tempdir_factory, 'types_or_repo') + with cwd(git_path): + stage_a_file('bar.notpy') + stage_a_file('bar.pxd') + stage_a_file('bar.py') + ret, printed = _do_run(cap_out, store, git_path, run_opts()) + assert ret == 1 + assert b'bar.notpy' not in printed + assert b'bar.pxd' in printed + assert b'bar.py' in printed + + def test_exclude_types_hook_repository(cap_out, store, tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'exclude_types_repo') with cwd(git_path): diff --git a/tests/repository_test.py b/tests/repository_test.py index 3d5093df6..8d771458f 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -901,6 +901,7 @@ def test_manifest_hooks(tempdir_factory, store): 'post-commit', 'manual', 'post-checkout', 'push', ), types=['file'], + types_or=['file'], verbose=False, ) From aa8023407e616eb77b6e8494bba4321fa1f3e6a5 Mon Sep 17 00:00:00 2001 From: Ruairidh MacLeod Date: Mon, 2 Nov 2020 14:01:46 +0000 Subject: [PATCH 439/967] fix dotnet build cleanup --- pre_commit/languages/dotnet.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index a8abc8611..094d2f1ce 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -12,7 +12,6 @@ from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure -from pre_commit.util import rmtree ENVIRONMENT_DIR = 'dotnetenv' BIN_DIR = 'bin' @@ -76,9 +75,9 @@ def install_environment( ), ) - # Cleanup build output - for d in ('bin', 'obj', build_dir): - rmtree(prefix.path(d)) + # Clean the git dir, ignoring the environment dir + clean_cmd = ('git', 'clean', '-ffxd', '-e', f'{ENVIRONMENT_DIR}-*') + helpers.run_setup_cmd(prefix, clean_cmd) def run_hook( From 64876697b5d6094fa132033f19647b99b631ad3f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 5 Nov 2020 15:59:46 -0800 Subject: [PATCH 440/967] use textwrap.indent instead of _indent --- pre_commit/commands/migrate_config.py | 8 ++------ tests/commands/migrate_config_test.py | 15 --------------- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index d580ff178..621c7e9ad 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -1,4 +1,5 @@ import re +import textwrap import yaml @@ -6,11 +7,6 @@ from pre_commit.util import yaml_load -def _indent(s: str) -> str: - lines = s.splitlines(True) - return ''.join(' ' * 4 + line if line.strip() else line for line in lines) - - def _is_header_line(line: str) -> bool: return line.startswith(('#', '---')) or not line.strip() @@ -34,7 +30,7 @@ def _migrate_map(contents: str) -> str: yaml_load(trial_contents) contents = trial_contents except yaml.YAMLError: - contents = f'{header}repos:\n{_indent(rest)}' + contents = f'{header}repos:\n{textwrap.indent(rest, " " * 4)}' return contents diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index 6a049d5f6..f5c89d044 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -2,24 +2,9 @@ import pre_commit.constants as C from pre_commit.clientlib import InvalidConfigError -from pre_commit.commands.migrate_config import _indent from pre_commit.commands.migrate_config import migrate_config -@pytest.mark.parametrize( - ('s', 'expected'), - ( - ('', ''), - ('a', ' a'), - ('foo\nbar', ' foo\n bar'), - ('foo\n\nbar\n', ' foo\n\n bar\n'), - ('\n\n\n', '\n\n\n'), - ), -) -def test_indent(s, expected): - assert _indent(s) == expected - - def test_migrate_config_normal_format(tmpdir, capsys): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write( From b4ab84df584b63799a903136346302e862155b89 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 5 Nov 2020 16:05:41 -0800 Subject: [PATCH 441/967] only perform migrate_config parsing if it is a list --- pre_commit/commands/migrate_config.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index d580ff178..1055c9f38 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -16,17 +16,17 @@ def _is_header_line(line: str) -> bool: def _migrate_map(contents: str) -> str: - # Find the first non-header line - lines = contents.splitlines(True) - i = 0 - # Only loop on non empty configuration file - while i < len(lines) and _is_header_line(lines[i]): - i += 1 + if isinstance(yaml_load(contents), list): + # Find the first non-header line + lines = contents.splitlines(True) + i = 0 + # Only loop on non empty configuration file + while i < len(lines) and _is_header_line(lines[i]): + i += 1 - header = ''.join(lines[:i]) - rest = ''.join(lines[i:]) + header = ''.join(lines[:i]) + rest = ''.join(lines[i:]) - if isinstance(yaml_load(contents), list): # If they are using the "default" flow style of yaml, this operation # will yield a valid configuration try: From 14f984fbcfaf60e76fe8006ef8a3323fd92b67bf Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 6 Nov 2020 13:09:01 -0800 Subject: [PATCH 442/967] improve xargs when running windows batch files --- pre_commit/xargs.py | 10 ++++++++++ tests/xargs_test.py | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 7538b54f6..60a057c19 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -137,6 +137,16 @@ def xargs( except parse_shebang.ExecutableNotFoundError as e: return e.to_output()[:2] + # on windows, batch files have a separate length limit than windows itself + if ( + sys.platform == 'win32' and + cmd[0].lower().endswith(('.bat', '.cmd')) + ): # pragma: win32 cover + # this is implementation details but the command gets translated into + # full/path/to/cmd.exe /c *cmd + cmd_exe = parse_shebang.find_executable('cmd.exe') + _max_length = 8192 - len(cmd_exe) - len(' /c ') + partitions = partition(cmd, varargs, target_concurrency, _max_length) def run_cmd_partition( diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 4f6136ede..7e83ef590 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -195,3 +195,12 @@ def test_xargs_color_true_makes_tty(): ) assert retcode == 0 assert out == b'True\n' + + +@pytest.mark.xfail(os.name == 'posix', reason='nt only') +@pytest.mark.parametrize('filename', ('t.bat', 't.cmd', 'T.CMD')) +def test_xargs_with_batch_files(tmpdir, filename): + f = tmpdir.join(filename) + f.write('echo it works\n') + retcode, out = xargs.xargs((str(f),), ('x',) * 8192) + assert retcode == 0, (retcode, out) From 64d57ba466a598bae8af765a509b233862b48846 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 6 Nov 2020 14:36:43 -0800 Subject: [PATCH 443/967] remove DOTALL on REV_LINE_RE --- pre_commit/commands/autoupdate.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 87f6d53d2..7320bb426 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -84,9 +84,7 @@ def _check_hooks_still_exist_at_rev( ) -REV_LINE_RE = re.compile( - r'^(\s+)rev:(\s*)([\'"]?)([^\s#]+)(.*)(\r?\n)$', re.DOTALL, -) +REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([\'"]?)([^\s#]+)(.*)(\r?\n)$') def _original_lines( From 13242f55c5c6c6b9cd8a3cf70627bf0c2b959d25 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 6 Nov 2020 16:29:56 -0800 Subject: [PATCH 444/967] add test to guard against yaml_dump --- tests/commands/autoupdate_test.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index bd89c1dba..b2bad6014 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -1,9 +1,12 @@ import shlex +from unittest import mock import pytest +import yaml import pre_commit.constants as C from pre_commit import git +from pre_commit import util from pre_commit.commands.autoupdate import _check_hooks_still_exist_at_rev from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.autoupdate import RepositoryCannotBeUpdatedError @@ -173,6 +176,11 @@ def test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store): assert cfg.read() == fmt.format(out_of_date.path, out_of_date.head_rev) +def test_autoupdate_pure_yaml(out_of_date, tmpdir, store): + with mock.patch.object(util, 'Dumper', yaml.SafeDumper): + test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store) + + def test_autoupdate_only_one_to_update(up_to_date, out_of_date, tmpdir, store): fmt = ( 'repos:\n' From 55cdfc6fd26f1b0392e3efdd26d1262b99fb143a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 9 Nov 2020 12:29:57 -0800 Subject: [PATCH 445/967] use slightly simpler enum syntax --- pre_commit/envcontext.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pre_commit/envcontext.py b/pre_commit/envcontext.py index 4ab0d8cb9..92d975d09 100644 --- a/pre_commit/envcontext.py +++ b/pre_commit/envcontext.py @@ -8,11 +8,7 @@ from typing import Tuple from typing import Union - -class _Unset(enum.Enum): - UNSET = 1 - - +_Unset = enum.Enum('_Unset', 'UNSET') UNSET = _Unset.UNSET From 6dbd53b3872ab58a8d4e8c347a2d1b40bb8c86a0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Nov 2020 17:04:58 +0000 Subject: [PATCH 446/967] [pre-commit.ci] pre-commit autoupdate --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 80fa14bbe..73692993b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,16 +21,16 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.7.1 + rev: v2.8.2 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.7.3 + rev: v2.7.4 hooks: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/asottile/reorder_python_imports - rev: v2.3.5 + rev: v2.3.6 hooks: - id: reorder-python-imports args: [--py3-plus] From a3e3b3d8aa37afd5d4806427e457540db10cfd43 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Nov 2020 11:58:46 -0800 Subject: [PATCH 447/967] fix for rbenv used outside of pre-commit and language_version: default --- pre_commit/languages/ruby.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 1a0f0c7e3..81bc95436 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -52,7 +52,6 @@ def get_env_patch( else: # pragma: win32 no cover patches += ( ('RBENV_ROOT', venv), - ('RBENV_VERSION', language_version), ( 'PATH', ( os.path.join(venv, 'gems', 'bin'), os.pathsep, @@ -61,6 +60,9 @@ def get_env_patch( ), ), ) + if language_version not in {'system', 'default'}: # pragma: win32 no cover + patches += (('RBENV_VERSION', language_version),) + return patches From 184e1908c88400e6e4a30b4f79276a6669fec26c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 19 Nov 2020 17:13:02 -0800 Subject: [PATCH 448/967] Add link to GitHub Sponsors + Open Collective at the time of writing I am currently unemployed. I'd love to make open source a full time career. if you or your company is deriving value from this free software, please consider [sponsoring] or [supporting]. [sponsoring]: https://github.com/sponsors/asottile [supporting]: https://opencollective.com/pre-commit Committed via https://github.com/asottile/all-repos --- .github/FUNDING.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..9408e44d6 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: asottile +open_collective: pre-commit From 120d60223a7bddc13bee956e8209b1ae281f31d3 Mon Sep 17 00:00:00 2001 From: Michael Vincent Date: Thu, 19 Nov 2020 23:09:53 -0600 Subject: [PATCH 449/967] Improve performance by ignoring submodules When git status runs in a repo with submodules, it'll recursively run git status in every submodule as well by default (sequentially). git status is substantially slower on Windows than on Linux. git diff behaves similarly to git status in terms of running recursively within all submodules. In repos with hundreds of submodules, this quickly adds up when git status/diff are called multiple times. Pre-commit runs git status once at the beginning of an operation and then runs git diff before and after each hook. These calls quickly add up and make pre-commit unusable in large repos with lots of submodules. This commit drastically improves performance in repos with lots of submodules and fixes #1701 by telling git status and git diff to ignore submodules. This change is not expected to have any negative effect on existing hooks because each submodule should manage its own hooks instead of relying on superproject hooks to manipulate their contents. --- pre_commit/commands/run.py | 4 +++- pre_commit/git.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 56450e384..508b61a34 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -258,7 +258,9 @@ def _all_filenames(args: argparse.Namespace) -> Collection[str]: def _get_diff() -> bytes: - _, out, _ = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None) + _, out, _ = cmd_output_b( + 'git', 'diff', '--no-ext-diff', '--ignore-submodules', retcode=None, + ) return out diff --git a/pre_commit/git.py b/pre_commit/git.py index 13ba664c8..8e22dcf0f 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -130,7 +130,9 @@ def get_staged_files(cwd: Optional[str] = None) -> List[str]: def intent_to_add_files() -> List[str]: - _, stdout, _ = cmd_output('git', 'status', '--porcelain', '-z') + _, stdout, _ = cmd_output( + 'git', 'status', '--ignore-submodules', '--porcelain', '-z', + ) parts = list(reversed(zsplit(stdout))) intent_to_add = [] while parts: From 099213f3657998df4028b493d53d87d59bbc126f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 21 Nov 2020 13:33:20 -0800 Subject: [PATCH 450/967] v2.9.0 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 26 ++++++++++++++++++++++++++ setup.cfg | 2 +- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 73692993b..72ce7bf42 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.8.2 + rev: v2.9.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index ff1013f82..4c7032b59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +2.9.0 - 2020-11-21 +================== + +### Features +- Add `types_or` which allows matching multiple disparate `types` in a hook + - #1677 by @MarcoGorelli. + - #607 by @asottile. +- Add Github Sponsors / Open Collective links + - https://github.com/sponsors/asottile + - https://opencollective.com/pre-commit + +### Fixes +- Improve cleanup for `language: dotnet` + - #1678 by @rkm. +- Fix "xargs" when running windows batch files + - #1686 PR by @asottile. + - #1604 issue by @apietrzak. + - #1604 issue by @ufwtlsb. +- Fix conflict with external `rbenv` and `language_version: default` + - #1700 PR by @asottile. + - #1699 issue by @abuxton. +- Improve performance of `git status` / `git diff` commands by ignoring + submodules + - #1704 PR by @Vynce. + - #1701 issue by @Vynce. + 2.8.2 - 2020-10-30 ================== diff --git a/setup.cfg b/setup.cfg index 32160b9ea..9b15fe1e6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.8.2 +version = 2.9.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 53109a0127c38f8d6ef57da45ccf1e78e353a10f Mon Sep 17 00:00:00 2001 From: Paul Fischer Date: Sun, 22 Nov 2020 22:31:42 +0100 Subject: [PATCH 451/967] fixed message if repo couldn't be updated due to missing hook(s) --- pre_commit/commands/autoupdate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 7320bb426..33a347302 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -79,8 +79,8 @@ def _check_hooks_still_exist_at_rev( hooks_missing = hooks - {hook['id'] for hook in manifest} if hooks_missing: raise RepositoryCannotBeUpdatedError( - f'Cannot update because the tip of HEAD is missing these hooks:\n' - f'{", ".join(sorted(hooks_missing))}', + f'Cannot update because the update target is missing these ' + f'hooks:\n{", ".join(sorted(hooks_missing))}', ) From 610716d3d1ca661a9487bac774992fb532c6a8e0 Mon Sep 17 00:00:00 2001 From: Paul Fischer Date: Sun, 22 Nov 2020 19:56:56 +0100 Subject: [PATCH 452/967] added warning if globs are used instead of regex --- pre_commit/clientlib.py | 14 ++++++++++++++ tests/clientlib_test.py | 17 +++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 0b8582bce..d619ea523 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -112,6 +112,18 @@ def validate_manifest_main(argv: Optional[Sequence[str]] = None) -> int: META = 'meta' +class OptionalSensibleRegex(cfgv.OptionalNoDefault): + def check(self, dct: Dict[str, Any]) -> None: + super().check(dct) + + if '/*' in dct.get(self.key, ''): + logger.warning( + f'The {self.key!r} field in hook {dct.get("id")!r} is a ' + f"regex, not a glob -- matching '/*' probably isn't what you " + f'want here', + ) + + class MigrateShaToRev: key = 'rev' @@ -227,6 +239,8 @@ def warn_unknown_keys_repo( for item in MANIFEST_HOOK_DICT.items if item.key != 'id' ), + OptionalSensibleRegex('files', cfgv.check_string), + OptionalSensibleRegex('exclude', cfgv.check_string), ) CONFIG_REPO_DICT = cfgv.Map( 'Repository', 'repo', diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 2e2f738c9..bfb754b66 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -166,6 +166,23 @@ def test_validate_warn_on_unknown_keys_at_top_level(tmpdir, caplog): ] +def test_validate_optional_sensible_regex(caplog): + config_obj = { + 'id': 'flake8', + 'files': 'dir/*.py', + } + cfgv.validate(config_obj, CONFIG_HOOK_DICT) + + assert caplog.record_tuples == [ + ( + 'pre_commit', + logging.WARNING, + "The 'files' field in hook 'flake8' is a regex, not a glob -- " + "matching '/*' probably isn't what you want here", + ), + ] + + @pytest.mark.parametrize('fn', (validate_config_main, validate_manifest_main)) def test_mains_not_ok(tmpdir, fn): not_yaml = tmpdir.join('f.notyaml') From 7486dee08286061a71ec4f8f9f2661949b13d8a2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 25 Nov 2020 12:42:58 -0800 Subject: [PATCH 453/967] fix for base executable with non-ascii characters on windows --- pre_commit/languages/python.py | 2 +- tests/languages/python_test.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 65f521cdc..43b728082 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -36,7 +36,7 @@ def _version_info(exe: str) -> str: def _read_pyvenv_cfg(filename: str) -> Dict[str, str]: ret = {} - with open(filename) as f: + with open(filename, encoding='UTF-8') as f: for line in f: try: k, v = line.split('=') diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index cfe14834f..90d1036a3 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -23,6 +23,13 @@ def test_read_pyvenv_cfg(tmpdir): assert python._read_pyvenv_cfg(pyvenv_cfg) == expected +def test_read_pyvenv_cfg_non_utf8(tmpdir): + pyvenv_cfg = tmpdir.join('pyvenv_cfg') + pyvenv_cfg.write_binary('hello = hello john.š\n'.encode()) + expected = {'hello': 'hello john.š'} + assert python._read_pyvenv_cfg(pyvenv_cfg) == expected + + def test_norm_version_expanduser(): home = os.path.expanduser('~') if os.name == 'nt': # pragma: nt cover From c4f2c6d24d73a1bd98cf9a6437a84ac7b3a1f4cd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 25 Nov 2020 13:40:28 -0800 Subject: [PATCH 454/967] v2.9.1 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 15 +++++++++++++++ setup.cfg | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 72ce7bf42..1b993e8c5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.9.0 + rev: v2.9.1 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c7032b59..9489b15d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +2.9.1 - 2020-11-25 +================== + +### Fixes +- Improve error message for "hook goes missing" + - #1709 PR by @paulhfischer. + - #1708 issue by @theod07. +- Add warning for `/*` in `files` / `exclude` regexes + - #1707 PR by @paulhfischer. + - #1702 issue by @asottile. +- Fix `healthy()` check for `language: python` on windows when the base + executable has non-ascii characters. + - #1713 PR by @asottile. + - #1711 issue by @Najiva. + 2.9.0 - 2020-11-21 ================== diff --git a/setup.cfg b/setup.cfg index 9b15fe1e6..9188df1b6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.9.0 +version = 2.9.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 0bd6dfc1a286e6bc98bee6ecb1d812c00486c85e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 25 Nov 2020 13:45:22 -0800 Subject: [PATCH 455/967] also produce sha256sum of pyz on creation --- testing/zipapp/make | 3 +++ 1 file changed, 3 insertions(+) diff --git a/testing/zipapp/make b/testing/zipapp/make index a644946d5..8740b2f5a 100755 --- a/testing/zipapp/make +++ b/testing/zipapp/make @@ -99,6 +99,9 @@ def main() -> int: shebang = '/usr/bin/env python3' zipapp.create_archive(tmpdir, filename, interpreter=shebang) + with open(f'{filename}.sha256sum', 'w') as f: + subprocess.check_call(('sha256sum', filename), stdout=f) + return 0 From 89ab609732ab5dfdcdc1ed7a374cf5c45126e523 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 25 Nov 2020 18:21:37 -0800 Subject: [PATCH 456/967] fix the default value for types_or --- pre_commit/clientlib.py | 2 +- pre_commit/commands/run.py | 6 +++++- tests/commands/run_test.py | 21 +++++++++++++++++++++ tests/repository_test.py | 2 +- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index d619ea523..20d44925a 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -61,7 +61,7 @@ def _make_argparser(filenames_help: str) -> argparse.ArgumentParser: cfgv.Optional('files', check_string_regex, ''), cfgv.Optional('exclude', check_string_regex, '^$'), cfgv.Optional('types', cfgv.check_array(check_type_tag), ['file']), - cfgv.Optional('types_or', cfgv.check_array(check_type_tag), ['file']), + cfgv.Optional('types_or', cfgv.check_array(check_type_tag), []), cfgv.Optional('exclude_types', cfgv.check_array(check_type_tag), []), cfgv.Optional( diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 508b61a34..1e8fad23b 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -92,7 +92,11 @@ def by_types( ret = [] for filename in names: tags = self._types_for_file(filename) - if tags >= types and tags & types_or and not tags & exclude_types: + if ( + tags >= types and + (not types_or or tags & types_or) and + not tags & exclude_types + ): ret.append(filename) return ret diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 34f3a3753..b4491d01f 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -964,6 +964,27 @@ def test_classifier_does_not_normalize_backslashes_non_windows(tmpdir): assert classifier.filenames == [r'a/b\c'] +def test_classifier_empty_types_or(tmpdir): + tmpdir.join('bar').ensure() + tmpdir.join('foo').mksymlinkto('bar') + with tmpdir.as_cwd(): + classifier = Classifier(('foo', 'bar')) + for_symlink = classifier.by_types( + classifier.filenames, + types=['symlink'], + types_or=[], + exclude_types=[], + ) + for_file = classifier.by_types( + classifier.filenames, + types=['file'], + types_or=[], + exclude_types=[], + ) + assert for_symlink == ['foo'] + assert for_file == ['bar'] + + @pytest.fixture def some_filenames(): return ( diff --git a/tests/repository_test.py b/tests/repository_test.py index 8d771458f..d513cb71f 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -901,7 +901,7 @@ def test_manifest_hooks(tempdir_factory, store): 'post-commit', 'manual', 'post-checkout', 'push', ), types=['file'], - types_or=['file'], + types_or=[], verbose=False, ) From f15cfbb2086018f502d02bb020bbbe367a76849e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 25 Nov 2020 18:39:54 -0800 Subject: [PATCH 457/967] v2.9.2 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1b993e8c5..f768a5b7e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.9.1 + rev: v2.9.2 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index 9489b15d5..d3773f6ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +2.9.2 - 2020-11-25 +================== + +### Fixes +- Fix default value for `types_or` so `symlink` and `directory` can be matched + - #1716 PR by @asottile. + - issue by code_bleu in [twitch chat](https://twitch.tv/anthonywritescode) + 2.9.1 - 2020-11-25 ================== diff --git a/setup.cfg b/setup.cfg index 9188df1b6..ed87cb1ac 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.9.1 +version = 2.9.2 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From e6c9b04386f496bd081ba12d78d80d4532acde6c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 26 Nov 2020 09:42:27 -0800 Subject: [PATCH 458/967] fix symlink test for windows --- tests/commands/run_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index b4491d01f..914d567a9 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -966,7 +966,7 @@ def test_classifier_does_not_normalize_backslashes_non_windows(tmpdir): def test_classifier_empty_types_or(tmpdir): tmpdir.join('bar').ensure() - tmpdir.join('foo').mksymlinkto('bar') + os.symlink(tmpdir.join('bar'), tmpdir.join('foo')) with tmpdir.as_cwd(): classifier = Classifier(('foo', 'bar')) for_symlink = classifier.by_types( From 6c6294571afef28e7229520c2beecc41400af60a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 27 Nov 2020 17:00:17 -0800 Subject: [PATCH 459/967] Add link to issue by CodeBleu --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3773f6ed..e833f9f3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Fixes - Fix default value for `types_or` so `symlink` and `directory` can be matched - #1716 PR by @asottile. - - issue by code_bleu in [twitch chat](https://twitch.tv/anthonywritescode) + - #1718 issue by @CodeBleu. 2.9.1 - 2020-11-25 ================== From 8cfe8e590d9568ff8fb9d5deb0c46776ee966162 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Nov 2020 15:16:52 -0800 Subject: [PATCH 460/967] don't crash on cygwin mismatch check --- pre_commit/git.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 8e22dcf0f..156e53d20 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -201,7 +201,10 @@ def check_for_cygwin_mismatch() -> None: """See https://github.com/pre-commit/pre-commit/issues/354""" if sys.platform in ('cygwin', 'win32'): # pragma: no cover (windows) is_cygwin_python = sys.platform == 'cygwin' - toplevel = get_root() + try: + toplevel = get_root() + except FatalError: # skip the check if we're not in a git repo + return is_cygwin_git = toplevel.startswith('/') if is_cygwin_python ^ is_cygwin_git: From bb0d9573a9a87616b65ee4c1cedccb18406d5982 Mon Sep 17 00:00:00 2001 From: francisco souza Date: Sat, 5 Dec 2020 22:26:38 -0500 Subject: [PATCH 461/967] util: also run chmod on EPERM Writing a test for this one is tricky, because I was seeing the issue only when the directory being removed is a docker volume, so instead of getting EACCES we get EPERM. This is easy to reproduce though. The existing test fails when the directory being used for the files is a docker volume: ``` % docker run \ -v $(mktemp -d):/tmp \ -v ${PWD}:/src \ -w /src \ python:3 \ bash -c 'pip install -e . && pip install -r requirements-dev.txt && python -m pytest tests/util_test.py' ``` --- pre_commit/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index f4cf7045a..fc506b98e 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -255,7 +255,7 @@ def handle_remove_readonly( excvalue = exc[1] if ( func in (os.rmdir, os.remove, os.unlink) and - excvalue.errno == errno.EACCES + excvalue.errno in (errno.EACCES, errno.EPERM) ): for p in (path, os.path.dirname(path)): os.chmod(p, os.stat(p).st_mode | stat.S_IWUSR) From c598785b6f7be0fb9371eb54e48fd09668210a96 Mon Sep 17 00:00:00 2001 From: francisco souza <108725+fsouza@users.noreply.github.com> Date: Sun, 6 Dec 2020 07:45:31 -0800 Subject: [PATCH 462/967] util: use set instead of tuple in errno check Co-authored-by: Paul Fischer <70564747+paulhfischer@users.noreply.github.com> --- pre_commit/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index fc506b98e..b5f40ada4 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -255,7 +255,7 @@ def handle_remove_readonly( excvalue = exc[1] if ( func in (os.rmdir, os.remove, os.unlink) and - excvalue.errno in (errno.EACCES, errno.EPERM) + excvalue.errno in {errno.EACCES, errno.EPERM} ): for p in (path, os.path.dirname(path)): os.chmod(p, os.stat(p).st_mode | stat.S_IWUSR) From 29d15de38ee0f78f598e8888fd3d6e8789a63bef Mon Sep 17 00:00:00 2001 From: Mark Rogaski Date: Sun, 6 Dec 2020 22:57:31 -0500 Subject: [PATCH 463/967] git: changed rev-parse option for Git 2.25 changes to --show-toplevel Git 2.25 introduced a change to "rev-parse --show-toplevel" that exposed underlying volumes for Windows drives mapped with SUBST. We use "rev-parse --show-cdup" to get the appropriate path, but must perform an extra check to see if we are in the .git directory. --- pre_commit/git.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 156e53d20..50962745b 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -47,21 +47,26 @@ def no_git_env( def get_root() -> str: + # Git 2.25 introduced a change to "rev-parse --show-toplevel" that exposed + # underlying volumes for Windows drives mapped with SUBST. We use + # "rev-parse --show-cdup" to get the appropriate path, but must perform + # an extra check to see if we are in the .git directory. try: - root = cmd_output('git', 'rev-parse', '--show-toplevel')[1].strip() + root = os.path.realpath( + cmd_output('git', 'rev-parse', '--show-cdup')[1].strip(), + ) + git_dir = os.path.realpath(get_git_dir()) except CalledProcessError: raise FatalError( 'git failed. Is it installed, and are you in a Git repository ' 'directory?', ) - else: - if root == '': # pragma: no cover (old git) - raise FatalError( - 'git toplevel unexpectedly empty! make sure you are not ' - 'inside the `.git` directory of your repository.', - ) - else: - return root + if os.path.commonpath((root, git_dir)) == git_dir: + raise FatalError( + 'git toplevel unexpectedly empty! make sure you are not ' + 'inside the `.git` directory of your repository.', + ) + return root def get_git_dir(git_root: str = '.') -> str: From a062cbd439861a8f05b58b9454ba04695de8cda3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 7 Dec 2020 15:06:39 -0800 Subject: [PATCH 464/967] v2.9.3 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 13 +++++++++++++ setup.cfg | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f768a5b7e..d42bb1b11 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.9.2 + rev: v2.9.3 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index e833f9f3d..ef36decce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +2.9.3 - 2020-12-07 +================== + +### Fixes +- Fix crash on cygwin mismatch check outside of a git directory + - #1721 PR by @asottile. + - #1720 issue by @chronoB. +- Fix cleanup code on docker volumes for go + - #1725 PR by @fsouza. +- Fix working directory detection on SUBST drives on windows + - #1727 PR by mrogaski. + - #1610 issue by @jcameron73. + 2.9.2 - 2020-11-25 ================== diff --git a/setup.cfg b/setup.cfg index ed87cb1ac..2e77fcf4e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.9.2 +version = 2.9.3 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 2d54ea112aab5568c149bb81f428dce70010a151 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 7 Dec 2020 15:09:02 -0800 Subject: [PATCH 465/967] fix typo in CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef36decce..c85c2c81c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ - Fix cleanup code on docker volumes for go - #1725 PR by @fsouza. - Fix working directory detection on SUBST drives on windows - - #1727 PR by mrogaski. + - #1727 PR by @mrogaski. - #1610 issue by @jcameron73. 2.9.2 - 2020-11-25 From 38a4a0aa3b8769976323162b435bfe8304c225f4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 31 May 2020 11:30:41 -0700 Subject: [PATCH 466/967] allow configuration for pre-commit.ci --- pre_commit/clientlib.py | 4 ++++ tests/clientlib_test.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 20d44925a..916c5ff23 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -297,9 +297,13 @@ def warn_unknown_keys_repo( 'exclude', 'fail_fast', 'minimum_pre_commit_version', + 'ci', ), warn_unknown_keys_root, ), + + # do not warn about configuration for pre-commit.ci + cfgv.OptionalNoDefault('ci', cfgv.check_type(dict)), ) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index bfb754b66..ba602362f 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -166,6 +166,20 @@ def test_validate_warn_on_unknown_keys_at_top_level(tmpdir, caplog): ] +def test_ci_map_key_allowed_at_top_level(caplog): + cfg = { + 'ci': {'skip': ['foo']}, + 'repos': [{'repo': 'meta', 'hooks': [{'id': 'identity'}]}], + } + cfgv.validate(cfg, CONFIG_SCHEMA) + assert not caplog.record_tuples + + +def test_ci_key_must_be_map(): + with pytest.raises(cfgv.ValidationError): + cfgv.validate({'ci': 'invalid', 'repos': []}, CONFIG_SCHEMA) + + def test_validate_optional_sensible_regex(caplog): config_obj = { 'id': 'flake8', From 1e4de986a8804aa620a001f4e04c9d4755e9d6b2 Mon Sep 17 00:00:00 2001 From: Paul Fischer Date: Tue, 8 Dec 2020 00:00:31 +0100 Subject: [PATCH 467/967] added warning if mutable rev is used --- pre_commit/clientlib.py | 28 ++++++++++++++++++ tests/clientlib_test.py | 64 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 916c5ff23..5dfaf7a3f 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -1,6 +1,7 @@ import argparse import functools import logging +import re import shlex import sys from typing import Any @@ -112,6 +113,25 @@ def validate_manifest_main(argv: Optional[Sequence[str]] = None) -> int: META = 'meta' +# should inherit from cfgv.Conditional if sha support is dropped +class WarnMutableRev(cfgv.ConditionalOptional): + def check(self, dct: Dict[str, Any]) -> None: + super().check(dct) + + if self.key in dct: + rev = dct[self.key] + + if '.' not in rev and not re.match(r'^[a-fA-F0-9]+$', rev): + logger.warning( + f'The {self.key!r} field of repo {dct["repo"]!r} ' + f'appears to be a mutable reference ' + f'(moving tag / branch). Mutable references are never ' + f'updated after first install and are not supported. ' + f'See https://pre-commit.com/#using-the-latest-version-for-a-repository ' # noqa: E501 + f'for more details.', + ) + + class OptionalSensibleRegex(cfgv.OptionalNoDefault): def check(self, dct: Dict[str, Any]) -> None: super().check(dct) @@ -261,6 +281,14 @@ def warn_unknown_keys_repo( ), MigrateShaToRev(), + WarnMutableRev( + 'rev', + cfgv.check_string, + '', + 'repo', + cfgv.NotIn(LOCAL, META), + True, + ), cfgv.WarnAdditionalKeys(('repo', 'rev', 'hooks'), warn_unknown_keys_repo), ) DEFAULT_LANGUAGE_VERSION = cfgv.Map( diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index ba602362f..d08ecdf05 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -180,6 +180,70 @@ def test_ci_key_must_be_map(): cfgv.validate({'ci': 'invalid', 'repos': []}, CONFIG_SCHEMA) +@pytest.mark.parametrize( + 'rev', + ( + 'v0.12.4', + 'b27f281', + 'b27f281eb9398fc8504415d7fbdabf119ea8c5e1', + '19.10b0', + '4.3.21-2', + ), +) +def test_warn_mutable_rev_ok(caplog, rev): + config_obj = { + 'repo': 'https://gitlab.com/pycqa/flake8', + 'rev': rev, + 'hooks': [{'id': 'flake8'}], + } + cfgv.validate(config_obj, CONFIG_REPO_DICT) + + assert caplog.record_tuples == [] + + +@pytest.mark.parametrize( + 'rev', + ( + '', + 'HEAD', + 'stable', + 'master', + 'some_branch_name', + ), +) +def test_warn_mutable_rev_invalid(caplog, rev): + config_obj = { + 'repo': 'https://gitlab.com/pycqa/flake8', + 'rev': rev, + 'hooks': [{'id': 'flake8'}], + } + cfgv.validate(config_obj, CONFIG_REPO_DICT) + + assert caplog.record_tuples == [ + ( + 'pre_commit', + logging.WARNING, + "The 'rev' field of repo 'https://gitlab.com/pycqa/flake8' " + 'appears to be a mutable reference (moving tag / branch). ' + 'Mutable references are never updated after first install and are ' + 'not supported. ' + 'See https://pre-commit.com/#using-the-latest-version-for-a-repository ' # noqa: E501 + 'for more details.', + ), + ] + + +def test_warn_mutable_rev_conditional(): + config_obj = { + 'repo': 'meta', + 'rev': '3.7.7', + 'hooks': [{'id': 'flake8'}], + } + + with pytest.raises(cfgv.ValidationError): + cfgv.validate(config_obj, CONFIG_REPO_DICT) + + def test_validate_optional_sensible_regex(caplog): config_obj = { 'id': 'flake8', From 75aa6a0840c46f43dbd2c6176103dc7d25977457 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 21 Dec 2020 16:43:31 +0000 Subject: [PATCH 468/967] [pre-commit.ci] pre-commit autoupdate --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d42bb1b11..0a3855243 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.3.0 + rev: v3.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -40,7 +40,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.15.1 + rev: v1.16.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy From b1a9209f9f9a4b4b8d6bdf5d2c0660d70c6b3312 Mon Sep 17 00:00:00 2001 From: Paul Fischer Date: Fri, 1 Jan 2021 23:57:24 +0100 Subject: [PATCH 469/967] extended warning if globs are used instead of regex to top level --- pre_commit/clientlib.py | 19 ++++++++++++++++--- tests/clientlib_test.py | 19 ++++++++++++++++++- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 5dfaf7a3f..8f35057d2 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -132,7 +132,7 @@ def check(self, dct: Dict[str, Any]) -> None: ) -class OptionalSensibleRegex(cfgv.OptionalNoDefault): +class OptionalSensibleRegexAtHook(cfgv.OptionalNoDefault): def check(self, dct: Dict[str, Any]) -> None: super().check(dct) @@ -144,6 +144,17 @@ def check(self, dct: Dict[str, Any]) -> None: ) +class OptionalSensibleRegexAtTop(cfgv.OptionalNoDefault): + def check(self, dct: Dict[str, Any]) -> None: + super().check(dct) + + if '/*' in dct.get(self.key, ''): + logger.warning( + f'The top-level {self.key!r} field is a regex, not a glob -- ' + f"matching '/*' probably isn't what you want here", + ) + + class MigrateShaToRev: key = 'rev' @@ -259,8 +270,8 @@ def warn_unknown_keys_repo( for item in MANIFEST_HOOK_DICT.items if item.key != 'id' ), - OptionalSensibleRegex('files', cfgv.check_string), - OptionalSensibleRegex('exclude', cfgv.check_string), + OptionalSensibleRegexAtHook('files', cfgv.check_string), + OptionalSensibleRegexAtHook('exclude', cfgv.check_string), ) CONFIG_REPO_DICT = cfgv.Map( 'Repository', 'repo', @@ -329,6 +340,8 @@ def warn_unknown_keys_repo( ), warn_unknown_keys_root, ), + OptionalSensibleRegexAtTop('files', cfgv.check_string), + OptionalSensibleRegexAtTop('exclude', cfgv.check_string), # do not warn about configuration for pre-commit.ci cfgv.OptionalNoDefault('ci', cfgv.check_type(dict)), diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index d08ecdf05..6bdb0d624 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -244,7 +244,7 @@ def test_warn_mutable_rev_conditional(): cfgv.validate(config_obj, CONFIG_REPO_DICT) -def test_validate_optional_sensible_regex(caplog): +def test_validate_optional_sensible_regex_at_hook_level(caplog): config_obj = { 'id': 'flake8', 'files': 'dir/*.py', @@ -261,6 +261,23 @@ def test_validate_optional_sensible_regex(caplog): ] +def test_validate_optional_sensible_regex_at_top_level(caplog): + config_obj = { + 'files': 'dir/*.py', + 'repos': [], + } + cfgv.validate(config_obj, CONFIG_SCHEMA) + + assert caplog.record_tuples == [ + ( + 'pre_commit', + logging.WARNING, + "The top-level 'files' field is a regex, not a glob -- matching " + "'/*' probably isn't what you want here", + ), + ] + + @pytest.mark.parametrize('fn', (validate_config_main, validate_manifest_main)) def test_mains_not_ok(tmpdir, fn): not_yaml = tmpdir.join('f.notyaml') From 42cc56c0f65e48398aef4a277e71a399bd52b79d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Jan 2021 16:43:53 +0000 Subject: [PATCH 470/967] [pre-commit.ci] pre-commit autoupdate --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0a3855243..649aca24b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: - id: reorder-python-imports args: [--py3-plus] - repo: https://github.com/asottile/add-trailing-comma - rev: v2.0.1 + rev: v2.0.2 hooks: - id: add-trailing-comma args: [--py36-plus] From d57207510dedd6303b32ba70374dc01d4880cc77 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 22 Jan 2021 12:26:22 -0800 Subject: [PATCH 471/967] fix reference to github.com/golang/example --- tests/repository_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/repository_test.py b/tests/repository_test.py index d513cb71f..860c6dc28 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -569,7 +569,7 @@ def test_additional_golang_dependencies_installed( path = make_repo(tempdir_factory, 'golang_hooks_repo') config = make_config_from_repo(path) # A small go package - deps = ['github.com/golang/example/hello'] + deps = ['golang.org/x/example/hello'] config['hooks'][0]['additional_dependencies'] = deps hook = _get_hook(config, store, 'golang-hook') binaries = os.listdir( @@ -590,7 +590,7 @@ def test_local_golang_additional_dependencies(store): 'name': 'hello', 'entry': 'hello', 'language': 'golang', - 'additional_dependencies': ['github.com/golang/example/hello'], + 'additional_dependencies': ['golang.org/x/example/hello'], }], } hook = _get_hook(config, store, 'hello') From cb5ed6276d334fa001443c49f189bb35c5246ac5 Mon Sep 17 00:00:00 2001 From: surafelabebe Date: Mon, 21 Dec 2020 15:16:11 -0800 Subject: [PATCH 472/967] Expose remote branch ref as an environment variable --- pre_commit/commands/hook_impl.py | 7 ++++++- pre_commit/commands/run.py | 3 ++- pre_commit/main.py | 3 +++ testing/util.py | 2 ++ tests/commands/run_test.py | 1 + 5 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index d0e226f8c..25c5fdffd 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -69,6 +69,7 @@ def _ns( color: bool, *, all_files: bool = False, + remote_branch: Optional[str] = None, from_ref: Optional[str] = None, to_ref: Optional[str] = None, remote_name: Optional[str] = None, @@ -79,6 +80,7 @@ def _ns( return argparse.Namespace( color=color, hook_stage=hook_type.replace('pre-', ''), + remote_branch=remote_branch, from_ref=from_ref, to_ref=to_ref, remote_name=remote_name, @@ -106,13 +108,14 @@ def _pre_push_ns( remote_url = args[1] for line in stdin.decode().splitlines(): - _, local_sha, _, remote_sha = line.split() + _, local_sha, remote_branch, remote_sha = line.split() if local_sha == Z40: continue elif remote_sha != Z40 and _rev_exists(remote_sha): return _ns( 'pre-push', color, from_ref=remote_sha, to_ref=local_sha, + remote_branch=remote_branch, remote_name=remote_name, remote_url=remote_url, ) else: @@ -133,6 +136,7 @@ def _pre_push_ns( 'pre-push', color, all_files=True, remote_name=remote_name, remote_url=remote_url, + remote_branch=remote_branch, ) else: rev_cmd = ('git', 'rev-parse', f'{first_ancestor}^') @@ -141,6 +145,7 @@ def _pre_push_ns( 'pre-push', color, from_ref=source, to_ref=local_sha, remote_name=remote_name, remote_url=remote_url, + remote_branch=remote_branch, ) # nothing to push diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 1e8fad23b..891488d59 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -371,7 +371,8 @@ def run( environ['PRE_COMMIT_FROM_REF'] = args.from_ref environ['PRE_COMMIT_TO_REF'] = args.to_ref - if args.remote_name and args.remote_url: + if args.remote_name and args.remote_url and args.remote_branch: + environ['PRE_COMMIT_REMOTE_BRANCH'] = args.remote_branch environ['PRE_COMMIT_REMOTE_NAME'] = args.remote_name environ['PRE_COMMIT_REMOTE_URL'] = args.remote_url diff --git a/pre_commit/main.py b/pre_commit/main.py index c1eb104ac..ce850c45c 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -96,6 +96,9 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: '--hook-stage', choices=C.STAGES, default='commit', help='The stage during which the hook is fired. One of %(choices)s', ) + parser.add_argument( + '--remote-branch', help='Remote branch ref used by `git push`.', + ) parser.add_argument( '--from-ref', '--source', '-s', help=( diff --git a/testing/util.py b/testing/util.py index 18cd73427..1f8cb35d2 100644 --- a/testing/util.py +++ b/testing/util.py @@ -61,6 +61,7 @@ def run_opts( color=False, verbose=False, hook=None, + remote_branch='', from_ref='', to_ref='', remote_name='', @@ -78,6 +79,7 @@ def run_opts( color=color, verbose=verbose, hook=hook, + remote_branch=remote_branch, from_ref=from_ref, to_ref=to_ref, remote_name=remote_name, diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 914d567a9..eaea8137c 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -486,6 +486,7 @@ def test_from_ref_to_ref_error_msg_error( def test_all_push_options_ok(cap_out, store, repo_with_passing_hook): args = run_opts( from_ref='master', to_ref='master', + remote_branch='master', remote_name='origin', remote_url='https://example.com/repo', ) ret, printed = _do_run(cap_out, store, repo_with_passing_hook, args) From 4f39946ea39007d357aa050126e8f877869ff719 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 22 Jan 2021 13:54:00 -0800 Subject: [PATCH 473/967] produce a more useful error message when non-installable things use language_version or additional_dependencies --- pre_commit/repository.py | 18 ++++++++++++++++ tests/repository_test.py | 46 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 46e96c1dc..15827dde4 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -118,6 +118,24 @@ def _hook( if not ret['stages']: ret['stages'] = root_config['default_stages'] + if languages[lang].ENVIRONMENT_DIR is None: + if ret['language_version'] != C.DEFAULT: + logger.error( + f'The hook `{ret["id"]}` specifies `language_version` but is ' + f'using language `{lang}` which does not install an ' + f'environment. ' + f'Perhaps you meant to use a specific language?', + ) + exit(1) + if ret['additional_dependencies']: + logger.error( + f'The hook `{ret["id"]}` specifies `additional_dependencies` ' + f'but is using language `{lang}` which does not install an ' + f'environment. ' + f'Perhaps you meant to use a specific language?', + ) + exit(1) + return ret diff --git a/tests/repository_test.py b/tests/repository_test.py index 860c6dc28..516f52e1d 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -942,3 +942,49 @@ def test_dotnet_hook(tempdir_factory, store, repo): tempdir_factory, store, repo, 'dotnet example hook', [], b'Hello from dotnet!\n', ) + + +def test_non_installable_hook_error_for_language_version(store, caplog): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'system-hook', + 'name': 'system-hook', + 'language': 'system', + 'entry': 'python3 -c "import sys; print(sys.version)"', + 'language_version': 'python3.10', + }], + } + with pytest.raises(SystemExit) as excinfo: + _get_hook(config, store, 'system-hook') + assert excinfo.value.code == 1 + + msg, = caplog.messages + assert msg == ( + 'The hook `system-hook` specifies `language_version` but is using ' + 'language `system` which does not install an environment. ' + 'Perhaps you meant to use a specific language?' + ) + + +def test_non_installable_hook_error_for_additional_dependencies(store, caplog): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'system-hook', + 'name': 'system-hook', + 'language': 'system', + 'entry': 'python3 -c "import sys; print(sys.version)"', + 'additional_dependencies': ['astpretty'], + }], + } + with pytest.raises(SystemExit) as excinfo: + _get_hook(config, store, 'system-hook') + assert excinfo.value.code == 1 + + msg, = caplog.messages + assert msg == ( + 'The hook `system-hook` specifies `additional_dependencies` but is ' + 'using language `system` which does not install an environment. ' + 'Perhaps you meant to use a specific language?' + ) From c7cbb1e6ad9fbb43d3124b20806322df9f3f59ff Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 22 Jan 2021 14:02:45 -0800 Subject: [PATCH 474/967] replace fake_log_handler with caplog --- tests/conftest.py | 9 --------- tests/repository_test.py | 15 ++++++++------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 335d2614f..b36ce5ac5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -261,15 +261,6 @@ def cap_out(): yield Fixture(stream) -@pytest.fixture -def fake_log_handler(): - handler = mock.Mock(level=logging.INFO) - logger = logging.getLogger('pre_commit') - logger.addHandler(handler) - yield handler - logger.removeHandler(handler) - - @pytest.fixture(scope='session', autouse=True) def set_git_templatedir(tmpdir_factory): tdir = str(tmpdir_factory.mktemp('git_template_dir')) diff --git a/tests/repository_test.py b/tests/repository_test.py index 860c6dc28..660bc6463 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -640,7 +640,7 @@ def test_fail_hooks(store): ) -def test_unknown_keys(store, fake_log_handler): +def test_unknown_keys(store, caplog): config = { 'repo': 'local', 'hooks': [{ @@ -653,8 +653,8 @@ def test_unknown_keys(store, fake_log_handler): }], } _get_hook(config, store, 'too-much') - expected = 'Unexpected key(s) present on local => too-much: foo, hello' - assert fake_log_handler.handle.call_args[0][0].msg == expected + msg, = caplog.messages + assert msg == 'Unexpected key(s) present on local => too-much: foo, hello' def test_reinstall(tempdir_factory, store, log_info_mock): @@ -832,27 +832,28 @@ def test_default_stages(store, local_python_config): assert hook.stages == ['push'] -def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): +def test_hook_id_not_present(tempdir_factory, store, caplog): path = make_repo(tempdir_factory, 'script_hooks_repo') config = make_config_from_repo(path) config['hooks'][0]['id'] = 'i-dont-exist' with pytest.raises(SystemExit): _get_hook(config, store, 'i-dont-exist') - assert fake_log_handler.handle.call_args[0][0].msg == ( + _, msg = caplog.messages + assert msg == ( f'`i-dont-exist` is not present in repository file://{path}. ' f'Typo? Perhaps it is introduced in a newer version? ' f'Often `pre-commit autoupdate` fixes this.' ) -def test_too_new_version(tempdir_factory, store, fake_log_handler): +def test_too_new_version(tempdir_factory, store, caplog): path = make_repo(tempdir_factory, 'script_hooks_repo') with modify_manifest(path) as manifest: manifest[0]['minimum_pre_commit_version'] = '999.0.0' config = make_config_from_repo(path) with pytest.raises(SystemExit): _get_hook(config, store, 'bash_hook') - msg = fake_log_handler.handle.call_args[0][0].msg + _, msg = caplog.messages pattern = re_assert.Matches( r'^The hook `bash_hook` requires pre-commit version 999\.0\.0 but ' r'version \d+\.\d+\.\d+ is installed. ' From 74183d91cbd24cac5dfd27dc0f737d40d665e0a8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Jan 2021 16:40:14 +0000 Subject: [PATCH 475/967] [pre-commit.ci] pre-commit autoupdate --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 649aca24b..cd5bd2637 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: - id: reorder-python-imports args: [--py3-plus] - repo: https://github.com/asottile/add-trailing-comma - rev: v2.0.2 + rev: v2.1.0 hooks: - id: add-trailing-comma args: [--py36-plus] @@ -44,7 +44,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.790 + rev: v0.800 hooks: - id: mypy exclude: ^testing/resources/ From d258650ad4a6b0c9845c82e8c74170df337ade41 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 25 Jan 2021 12:47:07 -0800 Subject: [PATCH 476/967] use comparison with sys.platform so mypy understands it --- pre_commit/file_lock.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index 5e7a05862..55a8eb29c 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -1,11 +1,11 @@ import contextlib import errno -import os +import sys from typing import Callable from typing import Generator -if os.name == 'nt': # pragma: no cover (windows) +if sys.platform == 'win32': # pragma: no cover (windows) import msvcrt # https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/locking From f75fc6b2a85a61e50a0270d24a3589a083f1c06c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 27 Jan 2021 10:08:48 -0800 Subject: [PATCH 477/967] fix execution in worktrees in subdirectories of bare repositories --- pre_commit/git.py | 2 +- tests/git_test.py | 20 ++++++++++++++++++++ tests/main_test.py | 5 ----- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 50962745b..bec816c94 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -61,7 +61,7 @@ def get_root() -> str: 'git failed. Is it installed, and are you in a Git repository ' 'directory?', ) - if os.path.commonpath((root, git_dir)) == git_dir: + if os.path.samefile(root, git_dir): raise FatalError( 'git toplevel unexpectedly empty! make sure you are not ' 'inside the `.git` directory of your repository.', diff --git a/tests/git_test.py b/tests/git_test.py index fafd4a6e3..69fd2067a 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -3,6 +3,7 @@ import pytest from pre_commit import git +from pre_commit.error_handler import FatalError from pre_commit.util import cmd_output from testing.util import git_commit @@ -18,6 +19,25 @@ def test_get_root_deeper(in_git_dir): assert os.path.normcase(git.get_root()) == expected +def test_in_exactly_dot_git(in_git_dir): + with in_git_dir.join('.git').as_cwd(), pytest.raises(FatalError): + git.get_root() + + +def test_get_root_bare_worktree(tmpdir): + src = tmpdir.join('src').ensure_dir() + cmd_output('git', 'init', str(src)) + git_commit(cwd=str(src)) + + bare = tmpdir.join('bare.git').ensure_dir() + cmd_output('git', 'clone', '--bare', str(src), str(bare)) + + cmd_output('git', 'worktree', 'add', 'foo', 'HEAD', cwd=bare) + + with bare.join('foo').as_cwd(): + assert git.get_root() == os.path.abspath('.') + + def test_get_staged_files_deleted(in_git_dir): in_git_dir.join('test').ensure() cmd_output('git', 'add', 'test') diff --git a/tests/main_test.py b/tests/main_test.py index 6738df683..2460bd85a 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -35,11 +35,6 @@ def test_adjust_args_and_chdir_not_in_git_dir(in_tmpdir): main._adjust_args_and_chdir(_args()) -def test_adjust_args_and_chdir_in_dot_git_dir(in_git_dir): - with in_git_dir.join('.git').as_cwd(), pytest.raises(FatalError): - main._adjust_args_and_chdir(_args()) - - def test_adjust_args_and_chdir_noop(in_git_dir): args = _args(command='run', files=['f1', 'f2']) main._adjust_args_and_chdir(args) From c67ba85311be53f4f0f830be22bc153524e07d03 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 27 Jan 2021 12:47:08 -0800 Subject: [PATCH 478/967] v2.10.0 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 24 ++++++++++++++++++++++++ setup.cfg | 2 +- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cd5bd2637..321e83962 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.9.3 + rev: v2.10.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index c85c2c81c..1ba7ffadd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,27 @@ +2.10.0 - 2021-01-27 +=================== + +### Features +- Allow `ci` as a top-level map for configuration for https://pre-commit.ci + - #1735 PR by @asottile. +- Add warning for mutable `rev` in configuration + - #1715 PR by @paulhfischer. + - #974 issue by @asottile. +- Add warning for `/*` in top-level `files` / `exclude` regexes + - #1750 PR by @paulhfischer. + - #1702 issue by @asottile. +- Expose `PRE_COMMIT_REMOTE_BRANCH` environment variable during `pre-push` + hooks + - #1770 PR by @surafelabebe. +- Produce error message for `language` / `language_version` for non-installable + languages + - #1771 PR by @asottile. + +### Fixes +- Fix execution in worktrees in subdirectories of bare repositories + - #1778 PR by @asottile. + - #1777 issue by @s0undt3ch. + 2.9.3 - 2020-12-07 ================== diff --git a/setup.cfg b/setup.cfg index 2e77fcf4e..913344bed 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.9.3 +version = 2.10.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 5e7c6eb31e1572a39661cd8cfe0f2866182ef75a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Feb 2021 16:48:53 +0000 Subject: [PATCH 479/967] [pre-commit.ci] pre-commit autoupdate --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 321e83962..66c04837e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.7.4 + rev: v2.9.0 hooks: - id: pyupgrade args: [--py36-plus] From 34e0ff349723310e954ddc25a528237cc769c66b Mon Sep 17 00:00:00 2001 From: Paul Fischer Date: Sat, 6 Feb 2021 19:03:57 +0100 Subject: [PATCH 480/967] added recursive repository support for golang --- pre_commit/languages/golang.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 91ade1e99..d6165d95e 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -69,7 +69,8 @@ def install_environment( repo_src_dir = os.path.join(directory, 'src', guess_go_dir(remote)) # Clone into the goenv we'll create - helpers.run_setup_cmd(prefix, ('git', 'clone', '.', repo_src_dir)) + cmd = ('git', 'clone', '--recursive', '.', repo_src_dir) + helpers.run_setup_cmd(prefix, cmd) if sys.platform == 'cygwin': # pragma: no cover _, gopath, _ = cmd_output('cygpath', '-w', directory) From 833bbf7186bbcb3940e08904c24f206b6c77918f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 6 Feb 2021 12:50:30 -0800 Subject: [PATCH 481/967] add test for recursive submodules for golang --- tests/repository_test.py | 54 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/repository_test.py b/tests/repository_test.py index 8540db3ce..1b58164cb 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -10,6 +10,7 @@ import re_assert import pre_commit.constants as C +from pre_commit import git from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest from pre_commit.envcontext import envcontext @@ -346,6 +347,59 @@ def test_golang_hook_still_works_when_gobin_is_set(tempdir_factory, store): assert os.listdir(gobin_dir) == [] +def test_golang_with_recursive_submodule(tmpdir, tempdir_factory, store): + sub_go = '''\ +package sub + +import "fmt" + +func Func() { + fmt.Println("hello hello world") +} +''' + sub = tmpdir.join('sub').ensure_dir() + sub.join('sub.go').write(sub_go) + cmd_output('git', '-C', str(sub), 'init', '.') + cmd_output('git', '-C', str(sub), 'add', '.') + git.commit(str(sub)) + + pre_commit_hooks = '''\ +- id: example + name: example + entry: example + language: golang + verbose: true +''' + go_mod = '''\ +module github.com/asottile/example + +go 1.14 +''' + main_go = '''\ +package main + +import "github.com/asottile/example/sub" + +func main() { + sub.Func() +} +''' + repo = tmpdir.join('repo').ensure_dir() + repo.join('.pre-commit-hooks.yaml').write(pre_commit_hooks) + repo.join('go.mod').write(go_mod) + repo.join('main.go').write(main_go) + cmd_output('git', '-C', str(repo), 'init', '.') + cmd_output('git', '-C', str(repo), 'add', '.') + cmd_output('git', '-C', str(repo), 'submodule', 'add', str(sub), 'sub') + git.commit(str(repo)) + + config = make_config_from_repo(str(repo)) + hook = _get_hook(config, store, 'example') + ret, out = _hook_run(hook, (), color=False) + assert ret == 0 + assert _norm_out(out) == b'hello hello world\n' + + def test_rust_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'rust_hooks_repo', From 0047fa35dd463aabe85fbd55bbd97ee03479f34c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 6 Feb 2021 13:21:12 -0800 Subject: [PATCH 482/967] v2.10.1 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 66c04837e..6cc66ebc3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.10.0 + rev: v2.10.1 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ba7ffadd..c8af449d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +2.10.1 - 2021-02-06 +=================== + +### Fixes +- Fix `language: golang` repositories containing recursive submodules + - #1788 issue by @gaurav517. + - #1789 PR by @paulhfischer. + 2.10.0 - 2021-01-27 =================== diff --git a/setup.cfg b/setup.cfg index 913344bed..7e4a1c4a4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.10.0 +version = 2.10.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 0cd8cbc83daa82dab68df2d821e650e0499af14e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Feb 2021 16:48:57 +0000 Subject: [PATCH 483/967] [pre-commit.ci] pre-commit autoupdate --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6cc66ebc3..95a8210c6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,12 +25,12 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.9.0 + rev: v2.10.0 hooks: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/asottile/reorder_python_imports - rev: v2.3.6 + rev: v2.4.0 hooks: - id: reorder-python-imports args: [--py3-plus] From c024147ede0f114bd4b1149a91d5c77793b8419a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Feb 2021 17:03:19 +0000 Subject: [PATCH 484/967] [pre-commit.ci] pre-commit autoupdate --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 95a8210c6..75d706663 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.800 + rev: v0.812 hooks: - id: mypy exclude: ^testing/resources/ From 3d31858ee3c92a58f02438c969c76630641981b1 Mon Sep 17 00:00:00 2001 From: "Jam M. Hernandez Quiceno" Date: Sat, 20 Feb 2021 17:40:38 -0500 Subject: [PATCH 485/967] Instruct users how to prevent a mutable rev in repo warning. --- pre_commit/clientlib.py | 3 ++- tests/clientlib_test.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 8f35057d2..962c7fa8f 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -128,7 +128,8 @@ def check(self, dct: Dict[str, Any]) -> None: f'(moving tag / branch). Mutable references are never ' f'updated after first install and are not supported. ' f'See https://pre-commit.com/#using-the-latest-version-for-a-repository ' # noqa: E501 - f'for more details.', + f'for more details. ' + f'Hint: `pre-commit autoupdate` often fixes this.', ) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 6bdb0d624..ff3cce38d 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -228,7 +228,8 @@ def test_warn_mutable_rev_invalid(caplog, rev): 'Mutable references are never updated after first install and are ' 'not supported. ' 'See https://pre-commit.com/#using-the-latest-version-for-a-repository ' # noqa: E501 - 'for more details.', + 'for more details. ' + 'Hint: `pre-commit autoupdate` often fixes this.', ), ] From 87dccbb8dc1190a6c7f54499165e3a73997f5c07 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 23 Feb 2021 18:22:12 -0800 Subject: [PATCH 486/967] fix _path_without_us under test when path segment ends in slash --- tests/commands/install_uninstall_test.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 7a4b90635..36615d11f 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -259,7 +259,10 @@ def _path_without_us(): exe = find_executable('pre-commit', _environ=env) while exe: parts = env['PATH'].split(os.pathsep) - after = [x for x in parts if x.lower() != os.path.dirname(exe).lower()] + after = [ + x for x in parts + if x.lower().rstrip(os.sep) != os.path.dirname(exe).lower() + ] if parts == after: raise AssertionError(exe, parts) env['PATH'] = os.pathsep.join(after) From f9fbe18abf44a55fb6a67b9601053fcee8979d12 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 23 Feb 2021 18:52:24 -0800 Subject: [PATCH 487/967] Fix pre-commit install on subst drives --- pre_commit/git.py | 4 ++-- tests/main_test.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index bec816c94..4bf282357 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -52,10 +52,10 @@ def get_root() -> str: # "rev-parse --show-cdup" to get the appropriate path, but must perform # an extra check to see if we are in the .git directory. try: - root = os.path.realpath( + root = os.path.abspath( cmd_output('git', 'rev-parse', '--show-cdup')[1].strip(), ) - git_dir = os.path.realpath(get_git_dir()) + git_dir = os.path.abspath(get_git_dir()) except CalledProcessError: raise FatalError( 'git failed. Is it installed, and are you in a Git repository ' diff --git a/tests/main_test.py b/tests/main_test.py index 2460bd85a..1ad8d418e 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -7,7 +7,9 @@ import pre_commit.constants as C from pre_commit import main from pre_commit.errors import FatalError +from pre_commit.util import cmd_output from testing.auto_namedtuple import auto_namedtuple +from testing.util import cwd @pytest.mark.parametrize( @@ -54,6 +56,17 @@ def test_adjust_args_and_chdir_relative_things(in_git_dir): assert args.files == [os.path.join('foo', 'f1'), os.path.join('foo', 'f2')] +@pytest.mark.skipif(os.name != 'nt', reason='windows feature') +def test_install_on_subst(in_git_dir, store): # pragma: posix no cover + assert not os.path.exists('Z:') + cmd_output('subst', 'Z:', str(in_git_dir)) + try: + with cwd('Z:'): + test_adjust_args_and_chdir_noop('Z:\\') + finally: + cmd_output('subst', '/d', 'Z:') + + def test_adjust_args_and_chdir_non_relative_config(in_git_dir): in_git_dir.join('foo').ensure_dir().chdir() From 6b73138c73efd0cb5bc923d7bdabfcd9ae36e6e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Sacawa?= Date: Thu, 21 Jan 2021 06:26:20 -0500 Subject: [PATCH 488/967] Add: post-merge hook support --- pre_commit/commands/hook_impl.py | 5 +++ pre_commit/commands/run.py | 5 ++- pre_commit/constants.py | 2 +- pre_commit/main.py | 11 +++++-- testing/util.py | 2 ++ tests/commands/hook_impl_test.py | 9 +++++ tests/commands/install_uninstall_test.py | 42 ++++++++++++++++++++++++ tests/commands/run_test.py | 9 +++++ tests/repository_test.py | 2 +- 9 files changed, 82 insertions(+), 5 deletions(-) diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index 25c5fdffd..a766ee9d6 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -76,6 +76,7 @@ def _ns( remote_url: Optional[str] = None, commit_msg_filename: Optional[str] = None, checkout_type: Optional[str] = None, + is_squash_merge: Optional[str] = None, ) -> argparse.Namespace: return argparse.Namespace( color=color, @@ -88,6 +89,7 @@ def _ns( commit_msg_filename=commit_msg_filename, all_files=all_files, checkout_type=checkout_type, + is_squash_merge=is_squash_merge, files=(), hook=None, verbose=False, @@ -158,6 +160,7 @@ def _pre_push_ns( 'post-commit': 0, 'pre-commit': 0, 'pre-merge-commit': 0, + 'post-merge': 1, 'pre-push': 2, } @@ -199,6 +202,8 @@ def _run_ns( hook_type, color, from_ref=args[0], to_ref=args[1], checkout_type=args[2], ) + elif hook_type == 'post-merge': + return _ns(hook_type, color, is_squash_merge=args[0]) else: raise AssertionError(f'unexpected hook type: {hook_type}') diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 891488d59..05c3268e3 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -245,7 +245,7 @@ def _compute_cols(hooks: Sequence[Hook]) -> int: def _all_filenames(args: argparse.Namespace) -> Collection[str]: # these hooks do not operate on files - if args.hook_stage in {'post-checkout', 'post-commit'}: + if args.hook_stage in {'post-checkout', 'post-commit', 'post-merge'}: return () elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}: return (args.commit_msg_filename,) @@ -379,6 +379,9 @@ def run( if args.checkout_type: environ['PRE_COMMIT_CHECKOUT_TYPE'] = args.checkout_type + if args.is_squash_merge: + environ['PRE_COMMIT_IS_SQUASH_MERGE'] = args.is_squash_merge + # Set pre_commit flag environ['PRE_COMMIT'] = '1' diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 5150fdcf4..3dcbbaca3 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -18,7 +18,7 @@ # `manual` is not invoked by any installed git hook. See #719 STAGES = ( 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', - 'post-commit', 'manual', 'post-checkout', 'push', + 'post-commit', 'manual', 'post-checkout', 'push', 'post-merge', ) DEFAULT = 'default' diff --git a/pre_commit/main.py b/pre_commit/main.py index ce850c45c..c66cfb9a4 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -67,8 +67,8 @@ def __call__( def _add_hook_type_option(parser: argparse.ArgumentParser) -> None: parser.add_argument( '-t', '--hook-type', choices=( - 'pre-commit', 'pre-merge-commit', 'pre-push', - 'prepare-commit-msg', 'commit-msg', 'post-commit', 'post-checkout', + 'pre-commit', 'pre-merge-commit', 'pre-push', 'prepare-commit-msg', + 'commit-msg', 'post-commit', 'post-checkout', 'post-merge', ), action=AppendReplaceDefault, default=['pre-commit'], @@ -136,6 +136,13 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: 'file from the index, flag=0).' ), ) + parser.add_argument( + '--is-squash-merge', + help=( + 'During a post-merge hook, indicates whether the merge was a ' + 'squash merge' + ), + ) def _adjust_args_and_chdir(args: argparse.Namespace) -> None: diff --git a/testing/util.py b/testing/util.py index 1f8cb35d2..13644531d 100644 --- a/testing/util.py +++ b/testing/util.py @@ -70,6 +70,7 @@ def run_opts( show_diff_on_failure=False, commit_msg_filename='', checkout_type='', + is_squash_merge='', ): # These are mutually exclusive assert not (all_files and files) @@ -88,6 +89,7 @@ def run_opts( show_diff_on_failure=show_diff_on_failure, commit_msg_filename=commit_msg_filename, checkout_type=checkout_type, + is_squash_merge=is_squash_merge, ) diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index 2fc014686..c38b9caa1 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -97,6 +97,7 @@ def call(*_, **__): ('pre-push', ['branch_name', 'remote_name']), ('commit-msg', ['.git/COMMIT_EDITMSG']), ('post-commit', []), + ('post-merge', ['1']), ('post-checkout', ['old_head', 'new_head', '1']), # multiple choices for commit-editmsg ('prepare-commit-msg', ['.git/COMMIT_EDITMSG']), @@ -157,6 +158,14 @@ def test_run_ns_post_commit(): assert ns.color is True +def test_run_ns_post_merge(): + ns = hook_impl._run_ns('post-merge', True, ('1',), b'') + assert ns is not None + assert ns.hook_stage == 'post-merge' + assert ns.color is True + assert ns.is_squash_merge == '1' + + def test_run_ns_post_checkout(): ns = hook_impl._run_ns('post-checkout', True, ('a', 'b', 'c'), b'') assert ns is not None diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 7a4b90635..c7d392ab8 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -759,6 +759,48 @@ def test_post_commit_integration(tempdir_factory, store): assert os.path.exists('post-commit.tmp') +def test_post_merge_integration(tempdir_factory, store): + path = git_dir(tempdir_factory) + config = [ + { + 'repo': 'local', + 'hooks': [{ + 'id': 'post-merge', + 'name': 'Post merge', + 'entry': 'touch post-merge.tmp', + 'language': 'system', + 'always_run': True, + 'verbose': True, + 'stages': ['post-merge'], + }], + }, + ] + write_config(path, config) + with cwd(path): + # create a simple diamond of commits for a non-trivial merge + open('init', 'a').close() + cmd_output('git', 'add', '.') + git_commit() + + open('master', 'a').close() + cmd_output('git', 'add', '.') + git_commit() + + cmd_output('git', 'checkout', '-b', 'branch', 'HEAD^') + open('branch', 'a').close() + cmd_output('git', 'add', '.') + git_commit() + + cmd_output('git', 'checkout', 'master') + install(C.CONFIG_FILE, store, hook_types=['post-merge']) + retc, stdout, stderr = cmd_output_mocked_pre_commit_home( + 'git', 'merge', 'branch', + tempdir_factory=tempdir_factory, + ) + assert retc == 0 + assert os.path.exists('post-merge.tmp') + + def test_post_checkout_integration(tempdir_factory, store): path = git_dir(tempdir_factory) config = [ diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index eaea8137c..4cd70fd43 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -494,6 +494,15 @@ def test_all_push_options_ok(cap_out, store, repo_with_passing_hook): assert b'Specify both --from-ref and --to-ref.' not in printed +def test_is_squash_merge(cap_out, store, repo_with_passing_hook): + args = run_opts(is_squash_merge='1') + environ: MutableMapping[str, str] = {} + ret, printed = _do_run( + cap_out, store, repo_with_passing_hook, args, environ, + ) + assert environ['PRE_COMMIT_IS_SQUASH_MERGE'] == '1' + + def test_checkout_type(cap_out, store, repo_with_passing_hook): args = run_opts(from_ref='', to_ref='', checkout_type='1') environ: MutableMapping[str, str] = {} diff --git a/tests/repository_test.py b/tests/repository_test.py index 1b58164cb..da678a32b 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -953,7 +953,7 @@ def test_manifest_hooks(tempdir_factory, store): require_serial=False, stages=( 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', - 'post-commit', 'manual', 'post-checkout', 'push', + 'post-commit', 'manual', 'post-checkout', 'push', 'post-merge', ), types=['file'], types_or=[], From fe1f56c08f6432cf281df797780d56992e7f9cca Mon Sep 17 00:00:00 2001 From: Rafik Draoui Date: Thu, 25 Feb 2021 19:11:21 -0400 Subject: [PATCH 489/967] Add support for Go 1.16 Go 1.16 changes the way modules are handled. It now expects Go projects to have non-empty `go.mod` files. This change is compatible with Go 1.15. Fixes #1815 --- pre_commit/resources/empty_template_go.mod | 1 + testing/resources/golang_hooks_repo/go.mod | 1 + 2 files changed, 2 insertions(+) create mode 100644 testing/resources/golang_hooks_repo/go.mod diff --git a/pre_commit/resources/empty_template_go.mod b/pre_commit/resources/empty_template_go.mod index e69de29bb..de3e24157 100644 --- a/pre_commit/resources/empty_template_go.mod +++ b/pre_commit/resources/empty_template_go.mod @@ -0,0 +1 @@ +module pre-commit-dummy-empty-module diff --git a/testing/resources/golang_hooks_repo/go.mod b/testing/resources/golang_hooks_repo/go.mod new file mode 100644 index 000000000..523bfc9f5 --- /dev/null +++ b/testing/resources/golang_hooks_repo/go.mod @@ -0,0 +1 @@ +module golang-hello-world From f1502119a2110ec858b9780eb6a6c04d28e5be6a Mon Sep 17 00:00:00 2001 From: Lorenz Date: Thu, 4 Feb 2021 00:22:44 +0100 Subject: [PATCH 490/967] add support for R via renv --- azure-pipelines.yml | 8 + pre_commit/languages/all.py | 2 + pre_commit/languages/r.py | 141 ++++++++++++++++++ pre_commit/resources/empty_template_renv.lock | 20 +++ pre_commit/store.py | 2 +- testing/gen-languages-all | 6 +- testing/get-r.ps1 | 6 + testing/get-r.sh | 9 ++ .../r_hooks_repo/.pre-commit-hooks.yaml | 48 ++++++ testing/resources/r_hooks_repo/DESCRIPTION | 19 +++ .../resources/r_hooks_repo/additional-deps.R | 2 + testing/resources/r_hooks_repo/hello-world.R | 5 + testing/resources/r_hooks_repo/renv.lock | 27 ++++ tests/languages/r_test.py | 104 +++++++++++++ tests/repository_test.py | 48 ++++++ 15 files changed, 443 insertions(+), 4 deletions(-) create mode 100644 pre_commit/languages/r.py create mode 100644 pre_commit/resources/empty_template_renv.lock create mode 100644 testing/get-r.ps1 create mode 100755 testing/get-r.sh create mode 100644 testing/resources/r_hooks_repo/.pre-commit-hooks.yaml create mode 100644 testing/resources/r_hooks_repo/DESCRIPTION create mode 100755 testing/resources/r_hooks_repo/additional-deps.R create mode 100755 testing/resources/r_hooks_repo/hello-world.R create mode 100644 testing/resources/r_hooks_repo/renv.lock create mode 100644 tests/languages/r_test.py diff --git a/azure-pipelines.yml b/azure-pipelines.yml index e7256da18..34ace234e 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -26,6 +26,10 @@ jobs: Write-Host "##vso[task.prependpath]C:\Strawberry\perl\site\bin" Write-Host "##vso[task.prependpath]C:\Strawberry\c\bin" displayName: Add strawberry perl to PATH + - task: PowerShell@2 + inputs: + filePath: "testing/get-r.ps1" + displayName: install R - template: job--python-tox.yml@asottile parameters: toxenvs: [py37] @@ -42,6 +46,8 @@ jobs: testing/get-swift.sh echo '##vso[task.prependpath]/tmp/swift/usr/bin' displayName: install swift + - bash: testing/get-r.sh + displayName: install R - template: job--python-tox.yml@asottile parameters: toxenvs: [pypy3, py36, py37, py38, py39] @@ -56,3 +62,5 @@ jobs: testing/get-swift.sh echo '##vso[task.prependpath]/tmp/swift/usr/bin' displayName: install swift + - bash: testing/get-r.sh + displayName: install R diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 9c2e59d78..fde6000cb 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -16,6 +16,7 @@ from pre_commit.languages import perl from pre_commit.languages import pygrep from pre_commit.languages import python +from pre_commit.languages import r from pre_commit.languages import ruby from pre_commit.languages import rust from pre_commit.languages import script @@ -52,6 +53,7 @@ class Language(NamedTuple): 'perl': Language(name='perl', ENVIRONMENT_DIR=perl.ENVIRONMENT_DIR, get_default_version=perl.get_default_version, healthy=perl.healthy, install_environment=perl.install_environment, run_hook=perl.run_hook), # noqa: E501 'pygrep': Language(name='pygrep', ENVIRONMENT_DIR=pygrep.ENVIRONMENT_DIR, get_default_version=pygrep.get_default_version, healthy=pygrep.healthy, install_environment=pygrep.install_environment, run_hook=pygrep.run_hook), # noqa: E501 'python': Language(name='python', ENVIRONMENT_DIR=python.ENVIRONMENT_DIR, get_default_version=python.get_default_version, healthy=python.healthy, install_environment=python.install_environment, run_hook=python.run_hook), # noqa: E501 + 'r': Language(name='r', ENVIRONMENT_DIR=r.ENVIRONMENT_DIR, get_default_version=r.get_default_version, healthy=r.healthy, install_environment=r.install_environment, run_hook=r.run_hook), # noqa: E501 'ruby': Language(name='ruby', ENVIRONMENT_DIR=ruby.ENVIRONMENT_DIR, get_default_version=ruby.get_default_version, healthy=ruby.healthy, install_environment=ruby.install_environment, run_hook=ruby.run_hook), # noqa: E501 'rust': Language(name='rust', ENVIRONMENT_DIR=rust.ENVIRONMENT_DIR, get_default_version=rust.get_default_version, healthy=rust.healthy, install_environment=rust.install_environment, run_hook=rust.run_hook), # noqa: E501 'script': Language(name='script', ENVIRONMENT_DIR=script.ENVIRONMENT_DIR, get_default_version=script.get_default_version, healthy=script.healthy, install_environment=script.install_environment, run_hook=script.run_hook), # noqa: E501 diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py new file mode 100644 index 000000000..1d42fea2d --- /dev/null +++ b/pre_commit/languages/r.py @@ -0,0 +1,141 @@ +import contextlib +import os +import shlex +import shutil +from typing import Generator +from typing import Sequence +from typing import Tuple + +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT +from pre_commit.hook import Hook +from pre_commit.languages import helpers +from pre_commit.prefix import Prefix +from pre_commit.util import clean_path_on_failure +from pre_commit.util import cmd_output_b + +ENVIRONMENT_DIR = 'renv' +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy + + +def get_env_patch(venv: str) -> PatchesT: + return ( + ('R_PROFILE_USER', os.path.join(venv, 'activate.R')), + ) + + +@contextlib.contextmanager +def in_env( + prefix: Prefix, + language_version: str, +) -> Generator[None, None, None]: + envdir = _get_env_dir(prefix, language_version) + with envcontext(get_env_patch(envdir)): + yield + + +def _get_env_dir(prefix: Prefix, version: str) -> str: + return prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) + + +def _prefix_if_file_entry( + entry: Sequence[str], + prefix: Prefix, +) -> Sequence[str]: + if entry[1] == '-e': + return entry[1:] + else: + return (prefix.path(entry[1]),) + + +def _entry_validate(entry: Sequence[str]) -> None: + """ + Allowed entries: + # Rscript -e expr + # Rscript path/to/file + """ + if entry[0] != 'Rscript': + raise ValueError('entry must start with `Rscript`.') + + if entry[1] == '-e': + if len(entry) > 3: + raise ValueError('You can supply at most one expression.') + elif len(entry) > 2: + raise ValueError( + 'The only valid syntax is `Rscript -e {expr}`', + 'or `Rscript path/to/hook/script`', + ) + + +def _cmd_from_hook(hook: Hook) -> Tuple[str, ...]: + opts = ('--no-save', '--no-restore', '--no-site-file', '--no-environ') + entry = shlex.split(hook.entry) + _entry_validate(entry) + + return ( + *entry[:1], *opts, + *_prefix_if_file_entry(entry, hook.prefix), + *hook.args, + ) + + +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: + env_dir = _get_env_dir(prefix, version) + with clean_path_on_failure(env_dir): + os.makedirs(env_dir, exist_ok=True) + path_desc_source = prefix.path('DESCRIPTION') + if os.path.exists(path_desc_source): + shutil.copy(path_desc_source, env_dir) + shutil.copy(prefix.path('renv.lock'), env_dir) + cmd_output_b( + 'Rscript', '--vanilla', '-e', + """\ + missing_pkgs <- setdiff( + "renv", unname(installed.packages()[, "Package"]) + ) + options( + repos = c(CRAN = "https://cran.rstudio.com"), + renv.consent = TRUE + ) + install.packages(missing_pkgs) + renv::activate() + renv::restore() + activate_statement <- paste0( + 'renv::activate("', file.path(getwd()), '"); ' + ) + writeLines(activate_statement, 'activate.R') + is_package <- tryCatch( + suppressWarnings( + unname(read.dcf('DESCRIPTION')[,'Type'] == "Package") + ), + error = function(...) FALSE + ) + if (is_package) { + renv::install(normalizePath('.')) + } + """, + cwd=env_dir, + ) + if additional_dependencies: + cmd_output_b( + 'Rscript', '-e', + 'renv::install(commandArgs(trailingOnly = TRUE))', + *additional_dependencies, + cwd=env_dir, + ) + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: + with in_env(hook.prefix, hook.language_version): + return helpers.run_xargs( + hook, _cmd_from_hook(hook), file_args, color=color, + ) diff --git a/pre_commit/resources/empty_template_renv.lock b/pre_commit/resources/empty_template_renv.lock new file mode 100644 index 000000000..d6e31f86c --- /dev/null +++ b/pre_commit/resources/empty_template_renv.lock @@ -0,0 +1,20 @@ +{ + "R": { + "Version": "4.0.3", + "Repositories": [ + { + "Name": "CRAN", + "URL": "https://cran.rstudio.com" + } + ] + }, + "Packages": { + "renv": { + "Package": "renv", + "Version": "0.12.5", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "5c0cdb37f063c58cdab3c7e9fbb8bd2c" + } + } +} diff --git a/pre_commit/store.py b/pre_commit/store.py index e5522ec33..187c9d353 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -189,7 +189,7 @@ def _git_cmd(*args: str) -> None: LOCAL_RESOURCES = ( 'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore', 'package.json', 'pre_commit_dummy_package.gemspec', 'setup.py', - 'environment.yml', 'Makefile.PL', + 'environment.yml', 'Makefile.PL', 'renv.lock', ) def make_local(self, deps: Sequence[str]) -> str: diff --git a/testing/gen-languages-all b/testing/gen-languages-all index d9b01bd04..eb7cd701e 100755 --- a/testing/gen-languages-all +++ b/testing/gen-languages-all @@ -2,9 +2,9 @@ import sys LANGUAGES = [ - 'conda', 'coursier', 'docker', 'dotnet', 'docker_image', 'fail', 'golang', - 'node', 'perl', 'pygrep', 'python', 'ruby', 'rust', 'script', 'swift', - 'system', + 'conda', 'coursier', 'docker', 'docker_image', 'dotnet', 'fail', 'golang', + 'node', 'perl', 'pygrep', 'python', 'r', 'ruby', 'rust', 'script', + 'swift', 'system', ] FIELDS = [ 'ENVIRONMENT_DIR', 'get_default_version', 'healthy', 'install_environment', diff --git a/testing/get-r.ps1 b/testing/get-r.ps1 new file mode 100644 index 000000000..e7b7b6195 --- /dev/null +++ b/testing/get-r.ps1 @@ -0,0 +1,6 @@ +$dir = $Env:Temp +$urlR = "https://cran.r-project.org/bin/windows/base/old/4.0.4/R-4.0.4-win.exe" +$outputR = "$dir\R-win.exe" +$wcR = New-Object System.Net.WebClient +$wcR.DownloadFile($urlR, $outputR) +Start-Process -FilePath $outputR -ArgumentList "/S /v/qn" diff --git a/testing/get-r.sh b/testing/get-r.sh new file mode 100755 index 000000000..5d09828e4 --- /dev/null +++ b/testing/get-r.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +sudo apt install r-base +# create empty folder for user library. +# necessary for non-root users who have +# never installed an R package before. +# Alternatively, we require the renv +# package to be installed already, then we can +# omit that. +Rscript -e 'dir.create(Sys.getenv("R_LIBS_USER"), recursive = TRUE)' diff --git a/testing/resources/r_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/r_hooks_repo/.pre-commit-hooks.yaml new file mode 100644 index 000000000..b3545d969 --- /dev/null +++ b/testing/resources/r_hooks_repo/.pre-commit-hooks.yaml @@ -0,0 +1,48 @@ +# parsing file +- id: parse-file-no-opts-no-args + name: Say hi + entry: Rscript parse-file-no-opts-no-args.R + language: r + types: [r] +- id: parse-file-no-opts-args + name: Say hi + entry: Rscript parse-file-no-opts-args.R + args: [--no-cache] + language: r + types: [r] +## parsing expr +- id: parse-expr-no-opts-no-args-1 + name: Say hi + entry: Rscript -e '1+1' + language: r + types: [r] +- id: parse-expr-args-in-entry-2 + name: Say hi + entry: Rscript -e '1+1' -e '3' --no-cache3 + language: r + types: [r] +# real world +- id: hello-world + name: Say hi + entry: Rscript hello-world.R + args: [blibla] + language: r + types: [r] +- id: hello-world-inline + name: Say hi + entry: | + Rscript -e + 'stopifnot( + packageVersion("rprojroot") == "1.0", + packageVersion("gli.clu") == "0.0.0.9000" + ) + cat(commandArgs(trailingOnly = TRUE), "from R!\n", sep = ", ") + ' + args: ['Hi-there'] + language: r + types: [r] +- id: additional-deps + name: Check additional deps + entry: Rscript additional-deps.R + language: r + types: [r] diff --git a/testing/resources/r_hooks_repo/DESCRIPTION b/testing/resources/r_hooks_repo/DESCRIPTION new file mode 100644 index 000000000..0e597a8a6 --- /dev/null +++ b/testing/resources/r_hooks_repo/DESCRIPTION @@ -0,0 +1,19 @@ +Package: gli.clu +Title: What the Package Does (One Line, Title Case) +Type: Package +Version: 0.0.0.9000 +Authors@R: + person(given = "First", + family = "Last", + role = c("aut", "cre"), + email = "first.last@example.com", + comment = c(ORCID = "YOUR-ORCID-ID")) +Description: What the package does (one paragraph). +License: `use_mit_license()`, `use_gpl3_license()` or friends to + pick a license +Encoding: UTF-8 +LazyData: true +Roxygen: list(markdown = TRUE) +RoxygenNote: 7.1.1 +Imports: + rprojroot diff --git a/testing/resources/r_hooks_repo/additional-deps.R b/testing/resources/r_hooks_repo/additional-deps.R new file mode 100755 index 000000000..bc145951b --- /dev/null +++ b/testing/resources/r_hooks_repo/additional-deps.R @@ -0,0 +1,2 @@ +suppressPackageStartupMessages(library("cachem")) +cat("OK\n") diff --git a/testing/resources/r_hooks_repo/hello-world.R b/testing/resources/r_hooks_repo/hello-world.R new file mode 100755 index 000000000..bf8d92f42 --- /dev/null +++ b/testing/resources/r_hooks_repo/hello-world.R @@ -0,0 +1,5 @@ +stopifnot( + packageVersion('rprojroot') == '1.0', + packageVersion('gli.clu') == '0.0.0.9000' +) +cat("Hello, World, from R!\n") diff --git a/testing/resources/r_hooks_repo/renv.lock b/testing/resources/r_hooks_repo/renv.lock new file mode 100644 index 000000000..d7d5fdcc9 --- /dev/null +++ b/testing/resources/r_hooks_repo/renv.lock @@ -0,0 +1,27 @@ +{ + "R": { + "Version": "4.0.3", + "Repositories": [ + { + "Name": "CRAN", + "URL": "https://cloud.r-project.org" + } + ] + }, + "Packages": { + "renv": { + "Package": "renv", + "Version": "0.12.5", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "5c0cdb37f063c58cdab3c7e9fbb8bd2c" + }, + "rprojroot": { + "Package": "rprojroot", + "Version": "1.0", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "86704667fe0860e4fec35afdfec137f3" + } + } +} diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py new file mode 100644 index 000000000..5c046efed --- /dev/null +++ b/tests/languages/r_test.py @@ -0,0 +1,104 @@ +import os.path + +import pytest + +from pre_commit.languages import r +from testing.fixtures import make_config_from_repo +from testing.fixtures import make_repo +from tests.repository_test import _get_hook_no_install + + +def _test_r_parsing( + tempdir_factory, + store, + hook_id, + expected_hook_expr={}, + expected_args={}, +): + repo_path = 'r_hooks_repo' + path = make_repo(tempdir_factory, repo_path) + config = make_config_from_repo(path) + hook = _get_hook_no_install(config, store, hook_id) + ret = r._cmd_from_hook(hook) + expected_cmd = 'Rscript' + expected_opts = ( + '--no-save', '--no-restore', '--no-site-file', '--no-environ', + ) + expected_path = os.path.join( + hook.prefix.prefix_dir, '.'.join([hook_id, 'R']), + ) + expected = ( + expected_cmd, + *expected_opts, + *(expected_hook_expr or (expected_path,)), + *expected_args, + ) + assert ret == expected + + +def test_r_parsing_file_no_opts_no_args(tempdir_factory, store): + hook_id = 'parse-file-no-opts-no-args' + _test_r_parsing(tempdir_factory, store, hook_id) + + +def test_r_parsing_file_opts_no_args(tempdir_factory, store): + with pytest.raises(ValueError) as excinfo: + r._entry_validate(['Rscript', '--no-init', '/path/to/file']) + + msg = excinfo.value.args + assert msg == ( + 'The only valid syntax is `Rscript -e {expr}`', + 'or `Rscript path/to/hook/script`', + ) + + +def test_r_parsing_file_no_opts_args(tempdir_factory, store): + hook_id = 'parse-file-no-opts-args' + expected_args = ['--no-cache'] + _test_r_parsing( + tempdir_factory, store, hook_id, expected_args=expected_args, + ) + + +def test_r_parsing_expr_no_opts_no_args1(tempdir_factory, store): + hook_id = 'parse-expr-no-opts-no-args-1' + _test_r_parsing( + tempdir_factory, store, hook_id, expected_hook_expr=('-e', '1+1'), + ) + + +def test_r_parsing_expr_no_opts_no_args2(tempdir_factory, store): + with pytest.raises(ValueError) as execinfo: + r._entry_validate(['Rscript', '-e', '1+1', '-e', 'letters']) + msg = execinfo.value.args + assert msg == ('You can supply at most one expression.',) + + +def test_r_parsing_expr_opts_no_args2(tempdir_factory, store): + with pytest.raises(ValueError) as execinfo: + r._entry_validate( + [ + 'Rscript', '--vanilla', '-e', '1+1', '-e', 'letters', + ], + ) + msg = execinfo.value.args + assert msg == ( + 'The only valid syntax is `Rscript -e {expr}`', + 'or `Rscript path/to/hook/script`', + ) + + +def test_r_parsing_expr_args_in_entry2(tempdir_factory, store): + with pytest.raises(ValueError) as execinfo: + r._entry_validate(['Rscript', '-e', 'expr1', '--another-arg']) + + msg = execinfo.value.args + assert msg == ('You can supply at most one expression.',) + + +def test_r_parsing_expr_non_Rscirpt(tempdir_factory, store): + with pytest.raises(ValueError) as execinfo: + r._entry_validate(['AnotherScript', '-e', '{{}}']) + + msg = execinfo.value.args + assert msg == ('entry must start with `Rscript`.',) diff --git a/tests/repository_test.py b/tests/repository_test.py index da678a32b..b6f7fb254 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -279,6 +279,54 @@ def test_node_hook_with_npm_userconfig_set(tempdir_factory, store, tmpdir): test_run_a_node_hook(tempdir_factory, store) +def test_r_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'r_hooks_repo', + 'hello-world', [os.devnull], + b'Hello, World, from R!\n', + ) + + +def test_r_inline_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'r_hooks_repo', + 'hello-world-inline', ['some-file'], + b'Hi-there, some-file, from R!\n', + ) + + +def test_r_with_additional_dependencies_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'r_hooks_repo', + 'additional-deps', [os.devnull], + b'OK\n', + config_kwargs={ + 'hooks': [{ + 'id': 'additional-deps', + 'additional_dependencies': ['cachem@1.0.4'], + }], + }, + ) + + +def test_r_local_with_additional_dependencies_hook(store): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'local-r', + 'name': 'local-r', + 'entry': 'Rscript -e', + 'language': 'r', + 'args': ['if (packageVersion("R6") == "2.1.3") cat("OK\n")'], + 'additional_dependencies': ['R6@2.1.3'], + }], + } + hook = _get_hook(config, store, 'local-r') + ret, out = _hook_run(hook, (), color=False) + assert ret == 0 + assert _norm_out(out) == b'OK\n' + + def test_run_a_ruby_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'ruby_hooks_repo', From 14d3af25ebc06da561e72e8a624919b7f8513f7c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 7 Mar 2021 14:43:32 -0800 Subject: [PATCH 491/967] add test for worktree inside of .git dir --- tests/git_test.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/git_test.py b/tests/git_test.py index 69fd2067a..51d5f8c43 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -38,6 +38,17 @@ def test_get_root_bare_worktree(tmpdir): assert git.get_root() == os.path.abspath('.') +def test_get_root_worktree_in_git(tmpdir): + src = tmpdir.join('src').ensure_dir() + cmd_output('git', 'init', str(src)) + git_commit(cwd=str(src)) + + cmd_output('git', 'worktree', 'add', '.git/trees/foo', 'HEAD', cwd=src) + + with src.join('.git/trees/foo').as_cwd(): + assert git.get_root() == os.path.abspath('.') + + def test_get_staged_files_deleted(in_git_dir): in_git_dir.join('test').ensure() cmd_output('git', 'add', 'test') From 54c49abbcb4b9fbf10865cc8e08f1628836a6088 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 7 Mar 2021 14:58:42 -0800 Subject: [PATCH 492/967] v2.11.0 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 20 ++++++++++++++++++++ setup.cfg | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 75d706663..2859e31f4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.10.1 + rev: v2.11.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index c8af449d3..eea586303 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +2.11.0 - 2021-03-07 +=================== + +### Features +- Improve warning for mutable ref. + - #1809 PR by @JamMarHer. +- Add support for `post-merge` hook. + - #1800 PR by @psacawa. + - #1762 issue by @psacawa. +- Add `r` as a supported hook language. + - #1799 PR by @lorenzwalthert. + +### Fixes +- Fix `pre-commit install` on `subst` / network drives on windows. + - #1814 PR by @asottile. + - #1802 issue by @goroderickgo. +- Fix installation of `local` golang repositories for go 1.16. + - #1818 PR by @rafikdraoui. + - #1815 issue by @rafikdraoui. + 2.10.1 - 2021-02-06 =================== diff --git a/setup.cfg b/setup.cfg index 7e4a1c4a4..5a4ee6e47 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.10.1 +version = 2.11.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From cf57e35e3753ebe62469998c3fbd6cf48acfc651 Mon Sep 17 00:00:00 2001 From: Lorenz Date: Mon, 8 Mar 2021 16:21:36 +0100 Subject: [PATCH 493/967] install package from prefix_dir, not env_dir (which yields empty pkg) --- pre_commit/languages/r.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 1d42fea2d..83e600094 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -88,13 +88,11 @@ def install_environment( env_dir = _get_env_dir(prefix, version) with clean_path_on_failure(env_dir): os.makedirs(env_dir, exist_ok=True) - path_desc_source = prefix.path('DESCRIPTION') - if os.path.exists(path_desc_source): - shutil.copy(path_desc_source, env_dir) shutil.copy(prefix.path('renv.lock'), env_dir) cmd_output_b( 'Rscript', '--vanilla', '-e', - """\ + f"""\ + prefix_dir <- {prefix.prefix_dir!r} missing_pkgs <- setdiff( "renv", unname(installed.packages()[, "Package"]) ) @@ -109,15 +107,15 @@ def install_environment( 'renv::activate("', file.path(getwd()), '"); ' ) writeLines(activate_statement, 'activate.R') - is_package <- tryCatch( - suppressWarnings( - unname(read.dcf('DESCRIPTION')[,'Type'] == "Package") - ), + is_package <- tryCatch({{ + content_desc <- read.dcf(file.path(prefix_dir, 'DESCRIPTION')) + suppressWarnings(unname(content_desc[,'Type']) == "Package") + }}, error = function(...) FALSE ) - if (is_package) { - renv::install(normalizePath('.')) - } + if (is_package) {{ + renv::install(prefix_dir) + }} """, cwd=env_dir, ) From 8aec369df752dc6bfc498a96eb922f656244070b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 9 Mar 2021 16:57:10 -0800 Subject: [PATCH 494/967] v2.11.1 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 7 +++++++ setup.cfg | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2859e31f4..bcfde9093 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.11.0 + rev: v2.11.1 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index eea586303..5da78662e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +2.11.1 - 2021-03-09 +=================== + +### Fixes +- Fix r hooks when hook repo is a package + - #1831 PR by @lorenzwalthert. + 2.11.0 - 2021-03-07 =================== diff --git a/setup.cfg b/setup.cfg index 5a4ee6e47..a14e95dbb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.11.0 +version = 2.11.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 74bbc72d28d67fcb8521d4b497ce318984e88bcb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Mar 2021 17:00:05 +0000 Subject: [PATCH 495/967] [pre-commit.ci] pre-commit autoupdate --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bcfde9093..39eb73834 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,12 +12,12 @@ repos: - id: requirements-txt-fixer - id: double-quote-string-fixer - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.4 + rev: 3.9.0 hooks: - id: flake8 additional_dependencies: [flake8-typing-imports==1.10.0] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.5.4 + rev: v1.5.5 hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit @@ -40,7 +40,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.16.0 + rev: v1.17.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy From 4a440f67c85d88fee0fee582bde393072ddc32f1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Mar 2021 17:00:34 +0000 Subject: [PATCH 496/967] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- setup.cfg | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/setup.cfg b/setup.cfg index a14e95dbb..ceb1cd4c2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -33,6 +33,11 @@ install_requires = importlib-resources;python_version<"3.7" python_requires = >=3.6.1 +[options.packages.find] +exclude = + tests* + testing* + [options.entry_points] console_scripts = pre-commit = pre_commit.main:main @@ -45,11 +50,6 @@ pre_commit.resources = empty_template_* hook-tmpl -[options.packages.find] -exclude = - tests* - testing* - [bdist_wheel] universal = True From e8cb09f70f53f83637685b2da15acc74026567dc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Mar 2021 17:03:06 +0000 Subject: [PATCH 497/967] [pre-commit.ci] pre-commit autoupdate --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 39eb73834..b63d5a9ad 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: flake8 additional_dependencies: [flake8-typing-imports==1.10.0] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.5.5 + rev: v1.5.6 hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.10.0 + rev: v2.11.0 hooks: - id: pyupgrade args: [--py36-plus] From 3bada745eab83ce19ecc683cce7d26d14d735e6d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 22 Mar 2021 19:41:30 -0700 Subject: [PATCH 498/967] upgrade ruby-build --- pre_commit/resources/ruby-build.tar.gz | Bin 72807 -> 74163 bytes .../make_archives.py => testing/make-archives | 23 +++++---- tests/make_archives_test.py | 46 ------------------ 3 files changed, 11 insertions(+), 58 deletions(-) rename pre_commit/make_archives.py => testing/make-archives (75%) mode change 100644 => 100755 delete mode 100644 tests/make_archives_test.py diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz index 4412ed40f48581b1c854711c49c8f991c2b0c4cf..c131f4a93a0af4f759bdf0cd15bcbfa6e5ed4aa5 100644 GIT binary patch literal 74163 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%0DX^HE0 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