From db46dc79bb372fd5a1d1c5b695b498f2938af0fd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 2 Jan 2020 10:28:45 -0800 Subject: [PATCH 01/49] 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 02/49] 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 03/49] 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 04/49] 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 05/49] 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 06/49] 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 07/49] 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 08/49] 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 09/49] 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 10/49] 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 11/49] 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 12/49] 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 13/49] 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 14/49] 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 15/49] 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 16/49] 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 17/49] 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 18/49] 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 19/49] 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 20/49] 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 21/49] 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 22/49] 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 23/49] 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 24/49] 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 25/49] 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 26/49] 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 27/49] 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 28/49] 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 29/49] 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 30/49] 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 31/49] 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 32/49] 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 33/49] 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 34/49] 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 35/49] 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 36/49] 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 37/49] 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 38/49] 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 39/49] 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 40/49] 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 41/49] 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 42/49] 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 43/49] 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 44/49] 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 45/49] 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 46/49] 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 47/49] 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 48/49] 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 49/49] 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