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

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

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

FB_o9 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%0Da>5o|PaFgyRW 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 Date: Mon, 23 Jan 2023 19:24:10 -0500 Subject: [PATCH 33/91] introduce install state v2 to replace v1 the v1 state is unnecessary since new repos are created for new additional_dependencies --- pre_commit/constants.py | 2 -- pre_commit/repository.py | 25 ++++++++++++++++++------- tests/repository_test.py | 16 ++++++++++++++++ 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 8fc5e55db..3f03ceed9 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -5,8 +5,6 @@ CONFIG_FILE = '.pre-commit-config.yaml' MANIFEST_FILE = '.pre-commit-hooks.yaml' -# Bump when installation changes in a backwards / forwards incompatible way -INSTALLED_STATE_VERSION = '1' # Bump when modifying `empty_template` LOCAL_REPO_VERSION = '1' diff --git a/pre_commit/repository.py b/pre_commit/repository.py index ac6b84463..616faf54c 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -23,16 +23,20 @@ logger = logging.getLogger('pre_commit') -def _state(additional_deps: Sequence[str]) -> object: - return {'additional_dependencies': sorted(additional_deps)} +def _state_filename_v1(venv: str) -> str: + return os.path.join(venv, '.install_state_v1') + + +def _state_filename_v2(venv: str) -> str: + return os.path.join(venv, '.install_state_v2') -def _state_filename(venv: str) -> str: - return os.path.join(venv, f'.install_state_v{C.INSTALLED_STATE_VERSION}') +def _state(additional_deps: Sequence[str]) -> object: + return {'additional_dependencies': sorted(additional_deps)} def _read_state(venv: str) -> object | None: - filename = _state_filename(venv) + filename = _state_filename_v1(venv) if not os.path.exists(filename): return None else: @@ -51,7 +55,10 @@ def _hook_installed(hook: Hook) -> bool: hook.language_version, ) return ( - _read_state(venv) == _state(hook.additional_dependencies) and + ( + os.path.exists(_state_filename_v2(venv)) or + _read_state(venv) == _state(hook.additional_dependencies) + ) and not lang.health_check(hook.prefix, hook.language_version) ) @@ -87,14 +94,18 @@ def _hook_install(hook: Hook) -> None: f'your environment\n\n' f'more info:\n\n{health_error}', ) + + # TODO: remove v1 state writing, no longer needed after pre-commit 3.0 # Write our state to indicate we're installed - state_filename = _state_filename(venv) + state_filename = _state_filename_v1(venv) staging = f'{state_filename}staging' with open(staging, 'w') as state_file: state_file.write(json.dumps(_state(hook.additional_dependencies))) # Move the file into place atomically to indicate we've installed os.replace(staging, state_filename) + open(_state_filename_v2(venv), 'a+').close() + def _hook( *hook_dicts: dict[str, Any], diff --git a/tests/repository_test.py b/tests/repository_test.py index 8d3034bb9..da8785963 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -23,6 +23,7 @@ from pre_commit.languages import rust from pre_commit.languages.all import languages from pre_commit.prefix import Prefix +from pre_commit.repository import _hook_installed from pre_commit.repository import all_hooks from pre_commit.repository import install_hook_envs from pre_commit.util import cmd_output @@ -562,6 +563,21 @@ def test_additional_dependencies_roll_forward(tempdir_factory, store): assert 'mccabe' not in cmd_output('pip', 'freeze', '-l')[1] +@pytest.mark.parametrize('v', ('v1', 'v2')) +def test_repository_state_compatibility(tempdir_factory, store, v): + path = make_repo(tempdir_factory, 'python_hooks_repo') + + config = make_config_from_repo(path) + hook = _get_hook(config, store, 'foo') + envdir = helpers.environment_dir( + hook.prefix, + python.ENVIRONMENT_DIR, + hook.language_version, + ) + os.remove(os.path.join(envdir, f'.install_state_{v}')) + assert _hook_installed(hook) is True + + def test_additional_ruby_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'ruby_hooks_repo') config = make_config_from_repo(path) From 6b88fe577c44472d234e8d4d8ee89ca36e03ae2a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 23 Jan 2023 20:40:13 -0500 Subject: [PATCH 34/91] v3.0.0 --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd0de5f73..59e0e202b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,37 @@ +3.0.0 - 2023-01-23 +================== + +### Features +- Make `language: golang` bootstrap `go` if not present. + - #2651 PR by @taoufik07. + - #2649 issue by @taoufik07. +- `language: coursier` now supports `additional_dependencies` and `repo: local` + - #2702 PR by @asottile. +- Upgrade `ruby-build` to `20221225`. + - #2718 PR by @jalessio. + +### Fixes +- Improve error message for invalid yaml for `pre-commit autoupdate`. + - #2686 PR by @asottile. + - #2685 issue by @CarstenGrohmann. +- `repo: local` no longer provisions an empty `git` repo. + - #2699 PR by @asottile. + +### Updating +- Drop support for python<3.8 + - #2655 PR by @asottile. +- Drop support for top-level list, use `pre-commit migrate-config` to update. + - #2656 PR by @asottile. +- Drop support for `sha` to specify revision, use `pre-commit migrate-config` + to update. + - #2657 PR by @asottile. +- Remove `pre-commit-validate-config` and `pre-commit-validate-manifest`, use + `pre-commit validate-config` and `pre-commit validate-manifest` instead. + - #2658 PR by @asottile. +- `language: golang` hooks must use `go.mod` to specify dependencies + - #2672 PR by @taoufik07. + + 2.21.0 - 2022-12-25 =================== diff --git a/setup.cfg b/setup.cfg index ca1f7d8bd..929f4c327 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.21.0 +version = 3.0.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 83e05e607e6b8cfde97c05e067d156be09a298a9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 25 Jan 2023 14:03:39 -0500 Subject: [PATCH 35/91] ensure coursier hooks are available offline after install --- pre_commit/languages/coursier.py | 45 ++++++++++++++++---------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index 69c877d32..60757588d 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -28,45 +28,44 @@ def install_environment( helpers.assert_version_default('coursier', version) # Support both possible executable names (either "cs" or "coursier") - executable = find_executable('cs') or find_executable('coursier') - if executable is None: + cs = find_executable('cs') or find_executable('coursier') + if cs is None: raise AssertionError( 'pre-commit requires system-installed "cs" or "coursier" ' 'executables in the application search path', ) envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) - channel = prefix.path('.pre-commit-channel') - if os.path.isdir(channel): - for app_descriptor in os.listdir(channel): - _, app_file = os.path.split(app_descriptor) - app, _ = os.path.splitext(app_file) - helpers.run_setup_cmd( - prefix, - ( - executable, - 'install', + + def _install(*opts: str) -> None: + assert cs is not None + helpers.run_setup_cmd(prefix, (cs, 'fetch', *opts)) + helpers.run_setup_cmd(prefix, (cs, 'install', '--dir', envdir, *opts)) + + with in_env(prefix, version): + channel = prefix.path('.pre-commit-channel') + if os.path.isdir(channel): + for app_descriptor in os.listdir(channel): + _, app_file = os.path.split(app_descriptor) + app, _ = os.path.splitext(app_file) + _install( '--default-channels=false', '--channel', channel, - '--dir', envdir, app, - ), + ) + elif not additional_dependencies: + raise FatalError( + 'expected .pre-commit-channel dir or additional_dependencies', ) - elif not additional_dependencies: - raise FatalError( - 'expected .pre-commit-channel dir or additional_dependencies', - ) - if additional_dependencies: - install_cmd = ( - executable, 'install', '--dir', envdir, *additional_dependencies, - ) - helpers.run_setup_cmd(prefix, install_cmd) + if additional_dependencies: + _install(*additional_dependencies) def get_env_patch(target_dir: str) -> PatchesT: return ( ('PATH', (target_dir, os.pathsep, Var('PATH'))), + ('COURSIER_CACHE', os.path.join(target_dir, '.cs-cache')), ) From dd8e717ed6022209a2b0cecf5c75460eb60e548e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 26 Jan 2023 11:09:17 -0500 Subject: [PATCH 36/91] v3.0.1 --- CHANGELOG.md | 7 +++++++ setup.cfg | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59e0e202b..d55ff7325 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +3.0.1 - 2023-01-26 +================== + +### Fixes +- Ensure coursier hooks are available offline after install. + - #2723 PR by @asottile. + 3.0.0 - 2023-01-23 ================== diff --git a/setup.cfg b/setup.cfg index 929f4c327..1dbace59c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.0.0 +version = 3.0.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From f4bd44996c888f48bc3a37d5ab19514325cb3f01 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Jan 2023 16:44:44 -0500 Subject: [PATCH 37/91] also ignore Gemfile in project this starts failing with ruby 3.2.0 --- pre_commit/languages/ruby.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 4416f7280..b4d4b45af 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -39,6 +39,7 @@ def get_env_patch( ('GEM_HOME', os.path.join(venv, 'gems')), ('GEM_PATH', UNSET), ('BUNDLE_IGNORE_CONFIG', '1'), + ('BUNDLE_GEMFILE', os.devnull), ) if language_version == 'system': patches += ( From 6e8051b9e644505f2755ed576cb4b7220f6db8b4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Jan 2023 16:20:50 -0500 Subject: [PATCH 38/91] speed up ruby tests by picking a prebuilt in 22.04 --- .../ruby_versioned_hooks_repo/.pre-commit-hooks.yaml | 2 +- tests/languages/ruby_test.py | 4 ++-- tests/repository_test.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml index 364d47d8f..c97939ad9 100644 --- a/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml +++ b/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml @@ -2,5 +2,5 @@ name: Ruby Hook entry: ruby_hook language: ruby - language_version: 3.1.0 + language_version: 3.2.0 files: \.rb$ diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 29f3c802e..63a16eb11 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -71,10 +71,10 @@ def test_install_ruby_default(fake_gem_prefix): @xfailif_windows # pragma: win32 no cover def test_install_ruby_with_version(fake_gem_prefix): - ruby.install_environment(fake_gem_prefix, '3.1.0', ()) + ruby.install_environment(fake_gem_prefix, '3.2.0', ()) # Should be able to activate and use rbenv install - with ruby.in_env(fake_gem_prefix, '3.1.0'): + with ruby.in_env(fake_gem_prefix, '3.2.0'): cmd_output('rbenv', 'install', '--help') diff --git a/tests/repository_test.py b/tests/repository_test.py index da8785963..ff2d7c323 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -247,7 +247,7 @@ def test_run_versioned_ruby_hook(tempdir_factory, store): tempdir_factory, store, 'ruby_versioned_hooks_repo', 'ruby_hook', [os.devnull], - b'3.1.0\nHello world from a ruby hook\n', + b'3.2.0\nHello world from a ruby hook\n', ) @@ -269,7 +269,7 @@ def test_run_ruby_hook_with_disable_shared_gems( tempdir_factory, store, 'ruby_versioned_hooks_repo', 'ruby_hook', [os.devnull], - b'3.1.0\nHello world from a ruby hook\n', + b'3.2.0\nHello world from a ruby hook\n', ) From 420902f67cbd2117e93f797191eaa9dab4be6904 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 29 Jan 2023 17:27:42 -0500 Subject: [PATCH 39/91] fix r local hooks `language: r` acts more like `language: script` so we have to *not* append the prefix when run with `repo: local` --- pre_commit/commands/run.py | 1 + pre_commit/languages/all.py | 1 + pre_commit/languages/docker.py | 1 + pre_commit/languages/docker_image.py | 1 + pre_commit/languages/fail.py | 1 + pre_commit/languages/helpers.py | 1 + pre_commit/languages/pygrep.py | 1 + pre_commit/languages/r.py | 17 ++++++++++++---- pre_commit/languages/script.py | 1 + testing/language_helpers.py | 2 ++ tests/languages/r_test.py | 29 ++++++++++++++++++++++++++-- tests/repository_test.py | 1 + 12 files changed, 51 insertions(+), 6 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 85fa59aa1..e44e70364 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -195,6 +195,7 @@ def _run_single_hook( hook.entry, hook.args, filenames, + is_local=hook.src == 'local', require_serial=hook.require_serial, color=use_color, ) diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index c7aab65e7..d952ae1ab 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -66,6 +66,7 @@ def run_hook( args: Sequence[str], file_args: Sequence[str], *, + is_local: bool, require_serial: bool, color: bool, ) -> tuple[int, bytes]: diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 18234567b..e80c95978 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -127,6 +127,7 @@ def run_hook( args: Sequence[str], file_args: Sequence[str], *, + is_local: bool, require_serial: bool, color: bool, ) -> tuple[int, bytes]: # pragma: win32 no cover diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index 230983823..8e5f2c04c 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -19,6 +19,7 @@ def run_hook( args: Sequence[str], file_args: Sequence[str], *, + is_local: bool, require_serial: bool, color: bool, ) -> tuple[int, bytes]: # pragma: win32 no cover diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index 13b2bc12c..33df067e4 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -18,6 +18,7 @@ def run_hook( args: Sequence[str], file_args: Sequence[str], *, + is_local: bool, require_serial: bool, color: bool, ) -> tuple[int, bytes]: diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 074f98e9f..d1be409c8 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -146,6 +146,7 @@ def basic_run_hook( args: Sequence[str], file_args: Sequence[str], *, + is_local: bool, require_serial: bool, color: bool, ) -> tuple[int, bytes]: diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 93e2a65bd..f0eb9a959 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -93,6 +93,7 @@ def run_hook( args: Sequence[str], file_args: Sequence[str], *, + is_local: bool, require_serial: bool, color: bool, ) -> tuple[int, bytes]: diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index dc3986057..e2383658a 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -35,8 +35,13 @@ def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: yield -def _prefix_if_file_entry(entry: list[str], prefix: Prefix) -> Sequence[str]: - if entry[1] == '-e': +def _prefix_if_file_entry( + entry: list[str], + prefix: Prefix, + *, + is_local: bool, +) -> Sequence[str]: + if entry[1] == '-e' or is_local: return entry[1:] else: return (prefix.path(entry[1]),) @@ -73,11 +78,14 @@ def _cmd_from_hook( prefix: Prefix, entry: str, args: Sequence[str], + *, + is_local: bool, ) -> tuple[str, ...]: cmd = shlex.split(entry) _entry_validate(cmd) - return (cmd[0], *RSCRIPT_OPTS, *_prefix_if_file_entry(cmd, prefix), *args) + cmd_part = _prefix_if_file_entry(cmd, prefix, is_local=is_local) + return (cmd[0], *RSCRIPT_OPTS, *cmd_part, *args) def install_environment( @@ -153,10 +161,11 @@ def run_hook( args: Sequence[str], file_args: Sequence[str], *, + is_local: bool, require_serial: bool, color: bool, ) -> tuple[int, bytes]: - cmd = _cmd_from_hook(prefix, entry, args) + cmd = _cmd_from_hook(prefix, entry, args, is_local=is_local) return helpers.run_xargs( cmd, file_args, diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 41fffdf07..08325f469 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -18,6 +18,7 @@ def run_hook( args: Sequence[str], file_args: Sequence[str], *, + is_local: bool, require_serial: bool, color: bool, ) -> tuple[int, bytes]: diff --git a/testing/language_helpers.py b/testing/language_helpers.py index 02e47a002..f9ae0b1da 100644 --- a/testing/language_helpers.py +++ b/testing/language_helpers.py @@ -16,6 +16,7 @@ def run_language( file_args: Sequence[str] = (), version: str = C.DEFAULT, deps: Sequence[str] = (), + is_local: bool = False, ) -> tuple[int, bytes]: prefix = Prefix(str(path)) @@ -26,6 +27,7 @@ def run_language( exe, args, file_args, + is_local=is_local, require_serial=True, color=False, ) diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py index 763fe8e9e..02c559cb4 100644 --- a/tests/languages/r_test.py +++ b/tests/languages/r_test.py @@ -14,7 +14,12 @@ def test_r_parsing_file_no_opts_no_args(tmp_path): - cmd = r._cmd_from_hook(Prefix(str(tmp_path)), 'Rscript some-script.R', ()) + cmd = r._cmd_from_hook( + Prefix(str(tmp_path)), + 'Rscript some-script.R', + (), + is_local=False, + ) assert cmd == ( 'Rscript', '--no-save', '--no-restore', '--no-site-file', '--no-environ', @@ -38,6 +43,7 @@ def test_r_parsing_file_no_opts_args(tmp_path): Prefix(str(tmp_path)), 'Rscript some-script.R', ('--no-cache',), + is_local=False, ) assert cmd == ( 'Rscript', @@ -48,7 +54,12 @@ def test_r_parsing_file_no_opts_args(tmp_path): def test_r_parsing_expr_no_opts_no_args1(tmp_path): - cmd = r._cmd_from_hook(Prefix(str(tmp_path)), "Rscript -e '1+1'", ()) + cmd = r._cmd_from_hook( + Prefix(str(tmp_path)), + "Rscript -e '1+1'", + (), + is_local=False, + ) assert cmd == ( 'Rscript', '--no-save', '--no-restore', '--no-site-file', '--no-environ', @@ -56,6 +67,20 @@ def test_r_parsing_expr_no_opts_no_args1(tmp_path): ) +def test_r_parsing_local_hook_path_is_not_expanded(tmp_path): + cmd = r._cmd_from_hook( + Prefix(str(tmp_path)), + 'Rscript path/to/thing.R', + (), + is_local=True, + ) + assert cmd == ( + 'Rscript', + '--no-save', '--no-restore', '--no-site-file', '--no-environ', + 'path/to/thing.R', + ) + + def test_r_parsing_expr_no_opts_no_args2(): with pytest.raises(ValueError) as excinfo: r._entry_validate(['Rscript', '-e', '1+1', '-e', 'letters']) diff --git a/tests/repository_test.py b/tests/repository_test.py index ff2d7c323..85cf45812 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -48,6 +48,7 @@ def _hook_run(hook, filenames, color): hook.entry, hook.args, filenames, + is_local=hook.src == 'local', require_serial=hook.require_serial, color=color, ) From 2adca78c6feb99d0e9b14158fa38e599ec7e84a6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 29 Jan 2023 18:27:10 -0500 Subject: [PATCH 40/91] test rust directly --- testing/language_helpers.py | 4 +- .../rust_hooks_repo/.pre-commit-hooks.yaml | 5 - testing/resources/rust_hooks_repo/Cargo.lock | 3 - testing/resources/rust_hooks_repo/Cargo.toml | 3 - testing/resources/rust_hooks_repo/src/main.rs | 3 - tests/languages/rust_test.py | 101 ++++++++++-------- tests/repository_test.py | 66 ------------ 7 files changed, 59 insertions(+), 126 deletions(-) delete mode 100644 testing/resources/rust_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/rust_hooks_repo/Cargo.lock delete mode 100644 testing/resources/rust_hooks_repo/Cargo.toml delete mode 100644 testing/resources/rust_hooks_repo/src/main.rs diff --git a/testing/language_helpers.py b/testing/language_helpers.py index 02e47a002..45fefbabd 100644 --- a/testing/language_helpers.py +++ b/testing/language_helpers.py @@ -3,7 +3,6 @@ import os from typing import Sequence -import pre_commit.constants as C from pre_commit.languages.all import Language from pre_commit.prefix import Prefix @@ -14,10 +13,11 @@ def run_language( exe: str, args: Sequence[str] = (), file_args: Sequence[str] = (), - version: str = C.DEFAULT, + version: str | None = None, deps: Sequence[str] = (), ) -> tuple[int, bytes]: prefix = Prefix(str(path)) + version = version or language.get_default_version() language.install_environment(prefix, version, deps) with language.in_env(prefix, version): diff --git a/testing/resources/rust_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/rust_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index df1269ff8..000000000 --- a/testing/resources/rust_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: rust-hook - name: rust example hook - entry: rust-hello-world - language: rust - files: '' diff --git a/testing/resources/rust_hooks_repo/Cargo.lock b/testing/resources/rust_hooks_repo/Cargo.lock deleted file mode 100644 index 36fbfda2b..000000000 --- a/testing/resources/rust_hooks_repo/Cargo.lock +++ /dev/null @@ -1,3 +0,0 @@ -[[package]] -name = "rust-hello-world" -version = "0.1.0" diff --git a/testing/resources/rust_hooks_repo/Cargo.toml b/testing/resources/rust_hooks_repo/Cargo.toml deleted file mode 100644 index cd83b4358..000000000 --- a/testing/resources/rust_hooks_repo/Cargo.toml +++ /dev/null @@ -1,3 +0,0 @@ -[package] -name = "rust-hello-world" -version = "0.1.0" diff --git a/testing/resources/rust_hooks_repo/src/main.rs b/testing/resources/rust_hooks_repo/src/main.rs deleted file mode 100644 index ad379d6ea..000000000 --- a/testing/resources/rust_hooks_repo/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("hello world"); -} diff --git a/tests/languages/rust_test.py b/tests/languages/rust_test.py index b8167a9e3..5c17f5b69 100644 --- a/tests/languages/rust_test.py +++ b/tests/languages/rust_test.py @@ -1,6 +1,5 @@ from __future__ import annotations -from typing import Mapping from unittest import mock import pytest @@ -8,8 +7,8 @@ import pre_commit.constants as C from pre_commit import parse_shebang from pre_commit.languages import rust -from pre_commit.prefix import Prefix -from pre_commit.util import cmd_output +from pre_commit.store import _make_local_repo +from testing.language_helpers import run_language ACTUAL_GET_DEFAULT_VERSION = rust.get_default_version.__wrapped__ @@ -30,64 +29,78 @@ def test_uses_default_when_rust_is_not_available(cmd_output_b_mck): assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT -@pytest.mark.parametrize('language_version', (C.DEFAULT, '1.56.0')) -def test_installs_with_bootstrapped_rustup(tmpdir, language_version): - tmpdir.join('src', 'main.rs').ensure().write( +def _make_hello_world(tmp_path): + src_dir = tmp_path.joinpath('src') + src_dir.mkdir() + src_dir.joinpath('main.rs').write_text( 'fn main() {\n' ' println!("Hello, world!");\n' '}\n', ) - tmpdir.join('Cargo.toml').ensure().write( + tmp_path.joinpath('Cargo.toml').write_text( '[package]\n' 'name = "hello_world"\n' 'version = "0.1.0"\n' 'edition = "2021"\n', ) - prefix = Prefix(str(tmpdir)) - find_executable_exes = [] - original_find_executable = parse_shebang.find_executable +def test_installs_rust_missing_rustup(tmp_path): + _make_hello_world(tmp_path) - def mocked_find_executable( - exe: str, *, env: Mapping[str, str] | None = None, - ) -> str | None: - """ - Return `None` the first time `find_executable` is called to ensure - that the bootstrapping code is executed, then just let the function - work as normal. + # pretend like `rustup` doesn't exist so it gets bootstrapped + calls = [] + orig = parse_shebang.find_executable - Also log the arguments to ensure that everything works as expected. - """ - find_executable_exes.append(exe) - if len(find_executable_exes) == 1: + def mck(exe, env=None): + calls.append(exe) + if len(calls) == 1: + assert exe == 'rustup' return None - return original_find_executable(exe, env=env) + return orig(exe, env=env) - with mock.patch.object(parse_shebang, 'find_executable') as find_exe_mck: - find_exe_mck.side_effect = mocked_find_executable - rust.install_environment(prefix, language_version, ()) - assert find_executable_exes == ['rustup', 'rustup', 'cargo'] + with mock.patch.object(parse_shebang, 'find_executable', side_effect=mck): + ret = run_language(tmp_path, rust, 'hello_world', version='1.56.0') + assert calls == ['rustup', 'rustup', 'cargo', 'hello_world'] + assert ret == (0, b'Hello, world!\n') - with rust.in_env(prefix, language_version): - assert cmd_output('hello_world')[1] == 'Hello, world!\n' +@pytest.mark.parametrize('version', (C.DEFAULT, '1.56.0')) +def test_language_version_with_rustup(tmp_path, version): + assert parse_shebang.find_executable('rustup') is not None -def test_installs_with_existing_rustup(tmpdir): - tmpdir.join('src', 'main.rs').ensure().write( - 'fn main() {\n' - ' println!("Hello, world!");\n' - '}\n', - ) - tmpdir.join('Cargo.toml').ensure().write( - '[package]\n' - 'name = "hello_world"\n' - 'version = "0.1.0"\n' - 'edition = "2021"\n', + _make_hello_world(tmp_path) + + ret = run_language(tmp_path, rust, 'hello_world', version=version) + assert ret == (0, b'Hello, world!\n') + + +@pytest.mark.parametrize('dep', ('cli:shellharden:4.2.0', 'cli:shellharden')) +def test_rust_cli_additional_dependencies(tmp_path, dep): + _make_local_repo(str(tmp_path)) + + t_sh = tmp_path.joinpath('t.sh') + t_sh.write_text('echo $hi\n') + + assert rust.get_default_version() == 'system' + ret = run_language( + tmp_path, + rust, + 'shellharden --transform', + deps=(dep,), + args=(str(t_sh),), ) - prefix = Prefix(str(tmpdir)) + assert ret == (0, b'echo "$hi"\n') - assert parse_shebang.find_executable('rustup') is not None - rust.install_environment(prefix, '1.56.0', ()) - with rust.in_env(prefix, '1.56.0'): - assert cmd_output('hello_world')[1] == 'Hello, world!\n' + +def test_run_lib_additional_dependencies(tmp_path): + _make_hello_world(tmp_path) + + deps = ('shellharden:4.2.0', 'git-version') + ret = run_language(tmp_path, rust, 'hello_world', deps=deps) + assert ret == (0, b'Hello, world!\n') + + bin_dir = tmp_path.joinpath('rustenv-system', 'bin') + assert bin_dir.is_dir() + assert not bin_dir.joinpath('shellharden').exists() + assert not bin_dir.joinpath('shellharden.exe').exists() diff --git a/tests/repository_test.py b/tests/repository_test.py index ff2d7c323..aea7ffbc7 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -20,7 +20,6 @@ from pre_commit.languages import node from pre_commit.languages import python from pre_commit.languages import ruby -from pre_commit.languages import rust from pre_commit.languages.all import languages from pre_commit.prefix import Prefix from pre_commit.repository import _hook_installed @@ -366,54 +365,6 @@ def test_golang_with_recursive_submodule(tmpdir, tempdir_factory, store): assert _norm_out(out) == b'hello hello world\n' -def test_rust_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'rust_hooks_repo', - 'rust-hook', [], b'hello world\n', - ) - - -@pytest.mark.parametrize('dep', ('cli:shellharden:3.1.0', 'cli:shellharden')) -def test_additional_rust_cli_dependencies_installed( - tempdir_factory, store, dep, -): - path = make_repo(tempdir_factory, 'rust_hooks_repo') - config = make_config_from_repo(path) - # A small rust package with no dependencies. - config['hooks'][0]['additional_dependencies'] = [dep] - hook = _get_hook(config, store, 'rust-hook') - envdir = helpers.environment_dir( - hook.prefix, - rust.ENVIRONMENT_DIR, - 'system', - ) - binaries = os.listdir(os.path.join(envdir, 'bin')) - # normalize for windows - binaries = [os.path.splitext(binary)[0] for binary in binaries] - assert 'shellharden' in binaries - - -def test_additional_rust_lib_dependencies_installed( - tempdir_factory, store, -): - path = make_repo(tempdir_factory, 'rust_hooks_repo') - config = make_config_from_repo(path) - # A small rust package with no dependencies. - deps = ['shellharden:3.1.0', 'git-version'] - config['hooks'][0]['additional_dependencies'] = deps - hook = _get_hook(config, store, 'rust-hook') - envdir = helpers.environment_dir( - hook.prefix, - rust.ENVIRONMENT_DIR, - 'system', - ) - binaries = os.listdir(os.path.join(envdir, 'bin')) - # normalize for windows - binaries = [os.path.splitext(binary)[0] for binary in binaries] - assert 'rust-hello-world' in binaries - assert 'shellharden' not in binaries - - def test_missing_executable(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'not_found_exe', @@ -636,23 +587,6 @@ def test_local_golang_additional_dependencies(store): assert _norm_out(out) == b'Hello, Go examples!\n' -def test_local_rust_additional_dependencies(store): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'hello', - 'name': 'hello', - 'entry': 'hello', - 'language': 'rust', - 'additional_dependencies': ['cli:hello-cli:0.2.2'], - }], - } - hook = _get_hook(config, store, 'hello') - ret, out = _hook_run(hook, (), color=False) - assert ret == 0 - assert _norm_out(out) == b'Hello World!\n' - - def test_fail_hooks(store): config = { 'repo': 'local', From 6abb05a60c4087a10c6ce196cd3a8bce065fa6f1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 29 Jan 2023 18:36:45 -0500 Subject: [PATCH 41/91] v3.0.2 --- CHANGELOG.md | 10 ++++++++++ setup.cfg | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d55ff7325..c0657e630 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +3.0.2 - 2023-01-29 +================== + +### Fixes +- Prevent local `Gemfile` from interfering with hook execution. + - #2727 PR by @asottile. +- Fix `language: r`, `repo: local` hooks + - pre-commit-ci/issues#107 by @lorenzwalthert. + - #2728 PR by @asottile. + 3.0.1 - 2023-01-26 ================== diff --git a/setup.cfg b/setup.cfg index 1dbace59c..37511c09e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.0.1 +version = 3.0.2 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 5b50acbd2c3f52f0e8dee3f11e08905430c4aef7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Jan 2023 21:36:13 -0500 Subject: [PATCH 42/91] test ruby directly --- testing/resources/ruby_hooks_repo/.gitignore | 1 - .../ruby_hooks_repo/.pre-commit-hooks.yaml | 5 - .../resources/ruby_hooks_repo/bin/ruby_hook | 3 - .../resources/ruby_hooks_repo/lib/.gitignore | 0 .../ruby_hooks_repo/ruby_hook.gemspec | 9 -- .../ruby_versioned_hooks_repo/.gitignore | 1 - .../.pre-commit-hooks.yaml | 6 - .../ruby_versioned_hooks_repo/bin/ruby_hook | 4 - .../ruby_versioned_hooks_repo/lib/.gitignore | 0 .../ruby_hook.gemspec | 9 -- tests/languages/ruby_test.py | 125 ++++++++++++------ tests/repository_test.py | 58 -------- 12 files changed, 87 insertions(+), 134 deletions(-) delete mode 100644 testing/resources/ruby_hooks_repo/.gitignore delete mode 100644 testing/resources/ruby_hooks_repo/.pre-commit-hooks.yaml delete mode 100755 testing/resources/ruby_hooks_repo/bin/ruby_hook delete mode 100644 testing/resources/ruby_hooks_repo/lib/.gitignore delete mode 100644 testing/resources/ruby_hooks_repo/ruby_hook.gemspec delete mode 100644 testing/resources/ruby_versioned_hooks_repo/.gitignore delete mode 100644 testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml delete mode 100755 testing/resources/ruby_versioned_hooks_repo/bin/ruby_hook delete mode 100644 testing/resources/ruby_versioned_hooks_repo/lib/.gitignore delete mode 100644 testing/resources/ruby_versioned_hooks_repo/ruby_hook.gemspec diff --git a/testing/resources/ruby_hooks_repo/.gitignore b/testing/resources/ruby_hooks_repo/.gitignore deleted file mode 100644 index c111b3313..000000000 --- a/testing/resources/ruby_hooks_repo/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.gem diff --git a/testing/resources/ruby_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/ruby_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index aa15872fb..000000000 --- a/testing/resources/ruby_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: ruby_hook - name: Ruby Hook - entry: ruby_hook - language: ruby - files: \.rb$ diff --git a/testing/resources/ruby_hooks_repo/bin/ruby_hook b/testing/resources/ruby_hooks_repo/bin/ruby_hook deleted file mode 100755 index 5a7e5ed25..000000000 --- a/testing/resources/ruby_hooks_repo/bin/ruby_hook +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env ruby - -puts 'Hello world from a ruby hook' diff --git a/testing/resources/ruby_hooks_repo/lib/.gitignore b/testing/resources/ruby_hooks_repo/lib/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/testing/resources/ruby_hooks_repo/ruby_hook.gemspec b/testing/resources/ruby_hooks_repo/ruby_hook.gemspec deleted file mode 100644 index 75f4e8f7d..000000000 --- a/testing/resources/ruby_hooks_repo/ruby_hook.gemspec +++ /dev/null @@ -1,9 +0,0 @@ -Gem::Specification.new do |s| - s.name = 'ruby_hook' - s.version = '0.1.0' - s.authors = ['Anthony Sottile'] - s.summary = 'A ruby hook!' - s.description = 'A ruby hook!' - s.files = ['bin/ruby_hook'] - s.executables = ['ruby_hook'] -end diff --git a/testing/resources/ruby_versioned_hooks_repo/.gitignore b/testing/resources/ruby_versioned_hooks_repo/.gitignore deleted file mode 100644 index c111b3313..000000000 --- a/testing/resources/ruby_versioned_hooks_repo/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.gem diff --git a/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index c97939ad9..000000000 --- a/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,6 +0,0 @@ -- id: ruby_hook - name: Ruby Hook - entry: ruby_hook - language: ruby - language_version: 3.2.0 - files: \.rb$ diff --git a/testing/resources/ruby_versioned_hooks_repo/bin/ruby_hook b/testing/resources/ruby_versioned_hooks_repo/bin/ruby_hook deleted file mode 100755 index 2406f04cf..000000000 --- a/testing/resources/ruby_versioned_hooks_repo/bin/ruby_hook +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env ruby - -puts RUBY_VERSION -puts 'Hello world from a ruby hook' diff --git a/testing/resources/ruby_versioned_hooks_repo/lib/.gitignore b/testing/resources/ruby_versioned_hooks_repo/lib/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/testing/resources/ruby_versioned_hooks_repo/ruby_hook.gemspec b/testing/resources/ruby_versioned_hooks_repo/ruby_hook.gemspec deleted file mode 100644 index 75f4e8f7d..000000000 --- a/testing/resources/ruby_versioned_hooks_repo/ruby_hook.gemspec +++ /dev/null @@ -1,9 +0,0 @@ -Gem::Specification.new do |s| - s.name = 'ruby_hook' - s.version = '0.1.0' - s.authors = ['Anthony Sottile'] - s.summary = 'A ruby hook!' - s.description = 'A ruby hook!' - s.files = ['bin/ruby_hook'] - s.executables = ['ruby_hook'] -end diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 63a16eb11..b312c7fda 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -1,6 +1,5 @@ from __future__ import annotations -import os.path import tarfile from unittest import mock @@ -8,10 +7,12 @@ import pre_commit.constants as C from pre_commit import parse_shebang +from pre_commit.envcontext import envcontext from pre_commit.languages import ruby -from pre_commit.prefix import Prefix -from pre_commit.util import cmd_output +from pre_commit.store import _make_local_repo from pre_commit.util import resource_bytesio +from testing.language_helpers import run_language +from testing.util import cwd from testing.util import xfailif_windows @@ -34,56 +35,104 @@ def test_uses_system_if_both_gem_and_ruby_are_available(find_exe_mck): assert ACTUAL_GET_DEFAULT_VERSION() == 'system' -@pytest.fixture -def fake_gem_prefix(tmpdir): +@pytest.mark.parametrize( + 'filename', + ('rbenv.tar.gz', 'ruby-build.tar.gz', 'ruby-download.tar.gz'), +) +def test_archive_root_stat(filename): + with resource_bytesio(filename) as f: + with tarfile.open(fileobj=f) as tarf: + root, _, _ = filename.partition('.') + assert oct(tarf.getmember(root).mode) == '0o755' + + +def _setup_hello_world(tmp_path): + bin_dir = tmp_path.joinpath('bin') + bin_dir.mkdir() + bin_dir.joinpath('ruby_hook').write_text( + '#!/usr/bin/env ruby\n' + "puts 'Hello world from a ruby hook'\n", + ) gemspec = '''\ Gem::Specification.new do |s| - s.name = 'pre_commit_placeholder_package' - s.version = '0.0.0' - s.summary = 'placeholder gem for pre-commit hooks' + s.name = 'ruby_hook' + s.version = '0.1.0' s.authors = ['Anthony Sottile'] + s.summary = 'A ruby hook!' + s.description = 'A ruby hook!' + s.files = ['bin/ruby_hook'] + s.executables = ['ruby_hook'] end ''' - tmpdir.join('placeholder_gem.gemspec').write(gemspec) - yield Prefix(tmpdir) + tmp_path.joinpath('ruby_hook.gemspec').write_text(gemspec) -@xfailif_windows # pragma: win32 no cover -def test_install_ruby_system(fake_gem_prefix): - ruby.install_environment(fake_gem_prefix, 'system', ()) +def test_ruby_hook_system(tmp_path): + assert ruby.get_default_version() == 'system' + + _setup_hello_world(tmp_path) + + ret = run_language(tmp_path, ruby, 'ruby_hook') + assert ret == (0, b'Hello world from a ruby hook\n') - # Should be able to activate and use rbenv install - with ruby.in_env(fake_gem_prefix, 'system'): - _, out, _ = cmd_output('gem', 'list') - assert 'pre_commit_placeholder_package' in out + +def test_ruby_with_user_install_set(tmp_path): + gemrc = tmp_path.joinpath('gemrc') + gemrc.write_text('gem: --user-install\n') + + with envcontext((('GEMRC', str(gemrc)),)): + test_ruby_hook_system(tmp_path) + + +def test_ruby_additional_deps(tmp_path): + _make_local_repo(tmp_path) + + ret = run_language( + tmp_path, + ruby, + 'ruby -e', + args=('require "tins"',), + deps=('tins',), + ) + assert ret == (0, b'') @xfailif_windows # pragma: win32 no cover -def test_install_ruby_default(fake_gem_prefix): - ruby.install_environment(fake_gem_prefix, C.DEFAULT, ()) - # Should have created rbenv directory - assert os.path.exists(fake_gem_prefix.path('rbenv-default')) +def test_ruby_hook_default(tmp_path): + _setup_hello_world(tmp_path) - # Should be able to activate using our script and access rbenv - with ruby.in_env(fake_gem_prefix, 'default'): - cmd_output('rbenv', '--help') + out, ret = run_language(tmp_path, ruby, 'rbenv --help', version='default') + assert out == 0 + assert ret.startswith(b'Usage: rbenv ') @xfailif_windows # pragma: win32 no cover -def test_install_ruby_with_version(fake_gem_prefix): - ruby.install_environment(fake_gem_prefix, '3.2.0', ()) +def test_ruby_hook_language_version(tmp_path): + _setup_hello_world(tmp_path) + tmp_path.joinpath('bin', 'ruby_hook').write_text( + '#!/usr/bin/env ruby\n' + 'puts RUBY_VERSION\n' + "puts 'Hello world from a ruby hook'\n", + ) - # Should be able to activate and use rbenv install - with ruby.in_env(fake_gem_prefix, '3.2.0'): - cmd_output('rbenv', 'install', '--help') + ret = run_language(tmp_path, ruby, 'ruby_hook', version='3.2.0') + assert ret == (0, b'3.2.0\nHello world from a ruby hook\n') -@pytest.mark.parametrize( - 'filename', - ('rbenv.tar.gz', 'ruby-build.tar.gz', 'ruby-download.tar.gz'), -) -def test_archive_root_stat(filename): - with resource_bytesio(filename) as f: - with tarfile.open(fileobj=f) as tarf: - root, _, _ = filename.partition('.') - assert oct(tarf.getmember(root).mode) == '0o755' +@xfailif_windows # pragma: win32 no cover +def test_ruby_with_bundle_disable_shared_gems(tmp_path): + workdir = tmp_path.joinpath('workdir') + workdir.mkdir() + # this Gemfile is missing `source` + workdir.joinpath('Gemfile').write_text('gem "lol_hai"\n') + # this bundle config causes things to be written elsewhere + bundle = workdir.joinpath('.bundle') + bundle.mkdir() + bundle.joinpath('config').write_text( + 'BUNDLE_DISABLE_SHARED_GEMS: true\n' + 'BUNDLE_PATH: vendor/gem\n', + ) + + with cwd(workdir): + # `3.2.0` has new enough `gem` requiring `source` and reading `.bundle` + test_ruby_hook_language_version(tmp_path) diff --git a/tests/repository_test.py b/tests/repository_test.py index 6565e1068..2cd4c0fa5 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -19,7 +19,6 @@ from pre_commit.languages import helpers from pre_commit.languages import node from pre_commit.languages import python -from pre_commit.languages import ruby from pre_commit.languages.all import languages from pre_commit.prefix import Prefix from pre_commit.repository import _hook_installed @@ -33,7 +32,6 @@ from testing.util import cwd from testing.util import get_resource_path from testing.util import skipif_cant_run_docker -from testing.util import xfailif_windows def _norm_out(b): @@ -227,52 +225,6 @@ def test_node_hook_with_npm_userconfig_set(tempdir_factory, store, tmpdir): test_run_a_node_hook(tempdir_factory, store) -def test_run_a_ruby_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'ruby_hooks_repo', - 'ruby_hook', [os.devnull], b'Hello world from a ruby hook\n', - ) - - -def test_run_a_ruby_hook_with_user_install_set(tempdir_factory, store, tmpdir): - gemrc = tmpdir.join('gemrc') - gemrc.write('gem: --user-install\n') - with envcontext((('GEMRC', str(gemrc)),)): - test_run_a_ruby_hook(tempdir_factory, store) - - -@xfailif_windows # pragma: win32 no cover -def test_run_versioned_ruby_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'ruby_versioned_hooks_repo', - 'ruby_hook', - [os.devnull], - b'3.2.0\nHello world from a ruby hook\n', - ) - - -@xfailif_windows # pragma: win32 no cover -def test_run_ruby_hook_with_disable_shared_gems( - tempdir_factory, - store, - tmpdir, -): - """Make sure a Gemfile in the project doesn't interfere.""" - tmpdir.join('Gemfile').write('gem "lol_hai"') - tmpdir.join('.bundle').mkdir() - tmpdir.join('.bundle', 'config').write( - 'BUNDLE_DISABLE_SHARED_GEMS: true\n' - 'BUNDLE_PATH: vendor/gem\n', - ) - with cwd(tmpdir.strpath): - _test_hook_repo( - tempdir_factory, store, 'ruby_versioned_hooks_repo', - 'ruby_hook', - [os.devnull], - b'3.2.0\nHello world from a ruby hook\n', - ) - - def test_system_hook_with_spaces(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'system_hook_with_spaces_repo', @@ -530,16 +482,6 @@ def test_repository_state_compatibility(tempdir_factory, store, v): assert _hook_installed(hook) is True -def test_additional_ruby_dependencies_installed(tempdir_factory, store): - path = make_repo(tempdir_factory, 'ruby_hooks_repo') - config = make_config_from_repo(path) - config['hooks'][0]['additional_dependencies'] = ['tins'] - hook = _get_hook(config, store, 'ruby_hook') - with ruby.in_env(hook.prefix, hook.language_version): - output = cmd_output('gem', 'list', '--local')[1] - assert 'tins' in output - - def test_additional_node_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'node_hooks_repo') config = make_config_from_repo(path) From f54386203eebe320175638b3c89dd71fdc2e8674 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Jan 2023 23:04:25 -0500 Subject: [PATCH 43/91] upgrade asottile/workflows to get fast-checkout --- .github/actions/pre-test/action.yml | 2 +- .github/workflows/main.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/pre-test/action.yml b/.github/actions/pre-test/action.yml index 608c0cd11..42bbf00b5 100644 --- a/.github/actions/pre-test/action.yml +++ b/.github/actions/pre-test/action.yml @@ -36,5 +36,5 @@ runs: testing/get-coursier.sh testing/get-dart.sh testing/get-swift.sh - - uses: asottile/workflows/.github/actions/latest-git@v1.2.0 + - uses: asottile/workflows/.github/actions/latest-git@v1.4.0 if: inputs.env == 'py38' && runner.os == 'Linux' diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c78d1051c..f281dcf27 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,12 +12,12 @@ concurrency: jobs: main-windows: - uses: asottile/workflows/.github/workflows/tox.yml@v1.2.0 + uses: asottile/workflows/.github/workflows/tox.yml@v1.4.0 with: env: '["py38"]' os: windows-latest main-linux: - uses: asottile/workflows/.github/workflows/tox.yml@v1.2.0 + uses: asottile/workflows/.github/workflows/tox.yml@v1.4.0 with: env: '["py38", "py39", "py310"]' os: ubuntu-latest From 2530913fad5c648d2614daf5c1a5583fb609fbd8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 31 Jan 2023 20:40:19 -0500 Subject: [PATCH 44/91] ensure languages are healthy after creation --- testing/language_helpers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/testing/language_helpers.py b/testing/language_helpers.py index b20803bce..b9c538403 100644 --- a/testing/language_helpers.py +++ b/testing/language_helpers.py @@ -21,6 +21,8 @@ def run_language( version = version or language.get_default_version() language.install_environment(prefix, version, deps) + health_error = language.health_check(prefix, version) + assert health_error is None, health_error with language.in_env(prefix, version): ret, out = language.run_hook( prefix, From 909dd0e8a1984300b37611a79cf33ad3dd92aa98 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 31 Jan 2023 19:37:37 -0500 Subject: [PATCH 45/91] test node directly --- .../node_hooks_repo/.pre-commit-hooks.yaml | 5 --- testing/resources/node_hooks_repo/bin/main.js | 3 -- .../resources/node_hooks_repo/package.json | 5 --- .../.pre-commit-hooks.yaml | 6 --- .../node_versioned_hooks_repo/bin/main.js | 4 -- .../node_versioned_hooks_repo/package.json | 5 --- tests/languages/node_test.py | 41 +++++++++++++++++ tests/repository_test.py | 44 ------------------- 8 files changed, 41 insertions(+), 72 deletions(-) delete mode 100644 testing/resources/node_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/node_hooks_repo/bin/main.js delete mode 100644 testing/resources/node_hooks_repo/package.json delete mode 100644 testing/resources/node_versioned_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/node_versioned_hooks_repo/bin/main.js delete mode 100644 testing/resources/node_versioned_hooks_repo/package.json diff --git a/testing/resources/node_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/node_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 257698a44..000000000 --- a/testing/resources/node_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: foo - name: Foo - entry: foo - language: node - files: \.js$ diff --git a/testing/resources/node_hooks_repo/bin/main.js b/testing/resources/node_hooks_repo/bin/main.js deleted file mode 100644 index 8e0f025ab..000000000 --- a/testing/resources/node_hooks_repo/bin/main.js +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env node - -console.log('Hello World'); diff --git a/testing/resources/node_hooks_repo/package.json b/testing/resources/node_hooks_repo/package.json deleted file mode 100644 index 050b6300b..000000000 --- a/testing/resources/node_hooks_repo/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "foo", - "version": "0.0.1", - "bin": {"foo": "./bin/main.js"} -} diff --git a/testing/resources/node_versioned_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/node_versioned_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index e7ad5ea7b..000000000 --- a/testing/resources/node_versioned_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,6 +0,0 @@ -- id: versioned-node-hook - name: Versioned node hook - entry: versioned-node-hook - language: node - language_version: 9.3.0 - files: \.js$ diff --git a/testing/resources/node_versioned_hooks_repo/bin/main.js b/testing/resources/node_versioned_hooks_repo/bin/main.js deleted file mode 100644 index df12cbebe..000000000 --- a/testing/resources/node_versioned_hooks_repo/bin/main.js +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env node - -console.log(process.version); -console.log('Hello World'); diff --git a/testing/resources/node_versioned_hooks_repo/package.json b/testing/resources/node_versioned_hooks_repo/package.json deleted file mode 100644 index 18c7787c7..000000000 --- a/testing/resources/node_versioned_hooks_repo/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "versioned-node-hook", - "version": "0.0.1", - "bin": {"versioned-node-hook": "./bin/main.js"} -} diff --git a/tests/languages/node_test.py b/tests/languages/node_test.py index b69adfa67..cba0228b3 100644 --- a/tests/languages/node_test.py +++ b/tests/languages/node_test.py @@ -13,7 +13,9 @@ from pre_commit import parse_shebang from pre_commit.languages import node from pre_commit.prefix import Prefix +from pre_commit.store import _make_local_repo from pre_commit.util import cmd_output +from testing.language_helpers import run_language from testing.util import xfailif_windows @@ -109,3 +111,42 @@ def test_installs_without_links_outside_env(tmpdir): with node.in_env(prefix, 'system'): assert cmd_output('foo')[1] == 'success!\n' + + +def _make_hello_world(tmp_path): + package_json = '''\ +{"name": "t", "version": "0.0.1", "bin": {"node-hello": "./bin/main.js"}} +''' + tmp_path.joinpath('package.json').write_text(package_json) + bin_dir = tmp_path.joinpath('bin') + bin_dir.mkdir() + bin_dir.joinpath('main.js').write_text( + '#!/usr/bin/env node\n' + 'console.log("Hello World");\n', + ) + + +def test_node_hook_system(tmp_path): + _make_hello_world(tmp_path) + ret = run_language(tmp_path, node, 'node-hello') + assert ret == (0, b'Hello World\n') + + +def test_node_with_user_config_set(tmp_path): + cfg = tmp_path.joinpath('cfg') + cfg.write_text('cache=/dne\n') + with envcontext.envcontext((('NPM_CONFIG_USERCONFIG', str(cfg)),)): + test_node_hook_system(tmp_path) + + +@pytest.mark.parametrize('version', (C.DEFAULT, '18.13.0')) +def test_node_hook_versions(tmp_path, version): + _make_hello_world(tmp_path) + ret = run_language(tmp_path, node, 'node-hello', version=version) + assert ret == (0, b'Hello World\n') + + +def test_node_additional_deps(tmp_path): + _make_local_repo(str(tmp_path)) + ret, out = run_language(tmp_path, node, 'npm ls -g', deps=('lodash',)) + assert b' lodash@' in out diff --git a/tests/repository_test.py b/tests/repository_test.py index 2cd4c0fa5..b43b344c8 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -17,7 +17,6 @@ from pre_commit.hook import Hook from pre_commit.languages import golang from pre_commit.languages import helpers -from pre_commit.languages import node from pre_commit.languages import python from pre_commit.languages.all import languages from pre_commit.prefix import Prefix @@ -193,38 +192,6 @@ def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): ) -def test_run_a_node_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'node_hooks_repo', - 'foo', [os.devnull], b'Hello World\n', - ) - - -def test_run_a_node_hook_default_version(tempdir_factory, store): - # make sure that this continues to work for platforms where node is not - # installed at the system - with mock.patch.object( - node, - 'get_default_version', - return_value=C.DEFAULT, - ): - test_run_a_node_hook(tempdir_factory, store) - - -def test_run_versioned_node_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'node_versioned_hooks_repo', - 'versioned-node-hook', [os.devnull], b'v9.3.0\nHello World\n', - ) - - -def test_node_hook_with_npm_userconfig_set(tempdir_factory, store, tmpdir): - cfg = tmpdir.join('cfg') - cfg.write('cache=/dne\n') - with mock.patch.dict(os.environ, NPM_CONFIG_USERCONFIG=str(cfg)): - test_run_a_node_hook(tempdir_factory, store) - - def test_system_hook_with_spaces(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'system_hook_with_spaces_repo', @@ -482,17 +449,6 @@ def test_repository_state_compatibility(tempdir_factory, store, v): assert _hook_installed(hook) is True -def test_additional_node_dependencies_installed(tempdir_factory, store): - path = make_repo(tempdir_factory, 'node_hooks_repo') - config = make_config_from_repo(path) - # Careful to choose a small package that's not depped by npm - config['hooks'][0]['additional_dependencies'] = ['lodash'] - hook = _get_hook(config, store, 'foo') - with node.in_env(hook.prefix, hook.language_version): - output = cmd_output('npm', 'ls', '-g')[1] - assert 'lodash' in output - - def test_additional_golang_dependencies_installed( tempdir_factory, store, ): From d216cdd5c1eccab623a71aa8b58813e4850f167d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 1 Feb 2023 18:16:09 -0500 Subject: [PATCH 46/91] fix golang version regex in test --- tests/languages/golang_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py index 0219261fb..7c04255bc 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -1,9 +1,9 @@ from __future__ import annotations -import re from unittest import mock import pytest +import re_assert import pre_commit.constants as C from pre_commit.languages import golang @@ -40,4 +40,4 @@ def test_golang_infer_go_version_default(): version = ACTUAL_INFER_GO_VERSION(C.DEFAULT) assert version != C.DEFAULT - assert re.match(r'^\d+\.\d+\.\d+$', version) + re_assert.Matches(r'^\d+\.\d+(?:\.\d+)?$').assert_matches(version) From 7260d24d0fb0577f2111626b25d4f7bba56bfa5d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 1 Feb 2023 17:52:53 -0500 Subject: [PATCH 47/91] Revert "also ignore Gemfile in project" This reverts commit f4bd44996c888f48bc3a37d5ab19514325cb3f01. --- pre_commit/languages/ruby.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index b4d4b45af..4416f7280 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -39,7 +39,6 @@ def get_env_patch( ('GEM_HOME', os.path.join(venv, 'gems')), ('GEM_PATH', UNSET), ('BUNDLE_IGNORE_CONFIG', '1'), - ('BUNDLE_GEMFILE', os.devnull), ) if language_version == 'system': patches += ( From 1129e7d222fea31c9c536da0ae41610349854128 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 1 Feb 2023 17:58:08 -0500 Subject: [PATCH 48/91] fixup Gemfile in ruby tests --- tests/languages/ruby_test.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index b312c7fda..9cfaad5d0 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -123,8 +123,9 @@ def test_ruby_hook_language_version(tmp_path): def test_ruby_with_bundle_disable_shared_gems(tmp_path): workdir = tmp_path.joinpath('workdir') workdir.mkdir() - # this Gemfile is missing `source` - workdir.joinpath('Gemfile').write_text('gem "lol_hai"\n') + # this needs a `source` or there's a deprecation warning + # silencing this with `BUNDLE_GEMFILE` breaks some tools (#2739) + workdir.joinpath('Gemfile').write_text('source ""\ngem "lol_hai"\n') # this bundle config causes things to be written elsewhere bundle = workdir.joinpath('.bundle') bundle.mkdir() @@ -134,5 +135,5 @@ def test_ruby_with_bundle_disable_shared_gems(tmp_path): ) with cwd(workdir): - # `3.2.0` has new enough `gem` requiring `source` and reading `.bundle` + # `3.2.0` has new enough `gem` reading `.bundle` test_ruby_hook_language_version(tmp_path) From e846829992a84ce8066e6513a72a357709eec56c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 1 Feb 2023 18:21:18 -0500 Subject: [PATCH 49/91] v3.0.3 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0657e630..adf1e4b39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +3.0.3 - 2023-02-01 +================== + +### Fixes +- Revert "Prevent local `Gemfile` from interfering with hook execution.". + - #2739 issue by @Roguelazer. + - #2740 PR by @asottile. + 3.0.2 - 2023-01-29 ================== diff --git a/setup.cfg b/setup.cfg index 37511c09e..8eb9de7ae 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.0.2 +version = 3.0.3 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 7783a3e63a18ea3fb073eef5412b985153abdee8 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 2 Feb 2023 11:02:58 +0000 Subject: [PATCH 50/91] Add `--no-textconv` to `git diff` calls --- pre_commit/commands/run.py | 6 +++--- tests/commands/run_test.py | 41 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index e44e70364..a7eb4f45a 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -272,7 +272,8 @@ def _all_filenames(args: argparse.Namespace) -> Collection[str]: def _get_diff() -> bytes: _, out, _ = cmd_output_b( - 'git', 'diff', '--no-ext-diff', '--ignore-submodules', check=False, + 'git', 'diff', '--no-ext-diff', '--no-textconv', '--ignore-submodules', + check=False, ) return out @@ -326,8 +327,7 @@ def _has_unmerged_paths() -> bool: def _has_unstaged_config(config_file: str) -> bool: retcode, _, _ = cmd_output_b( - 'git', 'diff', '--no-ext-diff', '--exit-code', config_file, - check=False, + 'git', 'diff', '--quiet', '--no-ext-diff', config_file, check=False, ) # be explicit, other git errors don't mean it has an unstaged config. return retcode == 1 diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 03d741e06..f1085d9bb 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -766,6 +766,47 @@ def test_lots_of_files(store, tempdir_factory): ) +def test_no_textconv(cap_out, store, repo_with_passing_hook): + # git textconv filters can hide changes from hooks + with open('.gitattributes', 'w') as fp: + fp.write('*.jpeg diff=empty\n') + + with open('.git/config', 'a') as fp: + fp.write('[diff "empty"]\n') + fp.write('textconv = "true"\n') + + config = { + 'repo': 'local', + 'hooks': [ + { + 'id': 'extend-jpeg', + 'name': 'extend-jpeg', + 'language': 'system', + 'entry': ( + f'{shlex.quote(sys.executable)} -c "import sys; ' + 'open(sys.argv[1], \'ab\').write(b\'\\x00\')"' + ), + 'types': ['jpeg'], + }, + ], + } + add_config_to_repo(repo_with_passing_hook, config) + + stage_a_file('example.jpeg') + + _test_run( + cap_out, + store, + repo_with_passing_hook, + {}, + ( + b'Failed', + ), + expected_ret=1, + stage=False, + ) + + def test_stages(cap_out, store, repo_with_passing_hook): config = { 'repo': 'local', From 0359fae2da2aadb2fbd3afae1777edd3aa856cc9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 3 Feb 2023 12:07:23 -0500 Subject: [PATCH 51/91] v3.0.4 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index adf1e4b39..0998da98b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +3.0.4 - 2023-02-03 +================== + +### Fixes +- Fix hook diff detection for files affected by `--textconv`. + - #2743 PR by @adamchainz. + - #2743 issue by @adamchainz. + 3.0.3 - 2023-02-01 ================== diff --git a/setup.cfg b/setup.cfg index 8eb9de7ae..56b856cad 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.0.3 +version = 3.0.4 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 0c1267b214cee6da7337f7bcd42b89fd13015e26 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 4 Feb 2023 14:26:09 -0500 Subject: [PATCH 52/91] deprecate python_venv language --- pre_commit/commands/migrate_config.py | 9 +++++ pre_commit/repository.py | 9 +++++ .../.pre-commit-hooks.yaml | 5 --- .../resources/python_venv_hooks_repo/foo.py | 9 ----- .../resources/python_venv_hooks_repo/setup.py | 10 ------ tests/commands/migrate_config_test.py | 33 +++++++++++++++++++ tests/languages/all_test.py | 7 ++++ tests/repository_test.py | 20 ++++++++--- 8 files changed, 73 insertions(+), 29 deletions(-) delete mode 100644 testing/resources/python_venv_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/python_venv_hooks_repo/foo.py delete mode 100644 testing/resources/python_venv_hooks_repo/setup.py create mode 100644 tests/languages/all_test.py diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index 6f7af4eba..842fb3a7b 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -42,6 +42,14 @@ def _migrate_sha_to_rev(contents: str) -> str: return re.sub(r'(\n\s+)sha:', r'\1rev:', contents) +def _migrate_python_venv(contents: str) -> str: + return re.sub( + r'(\n\s+)language: python_venv\b', + r'\1language: python', + contents, + ) + + def migrate_config(config_file: str, quiet: bool = False) -> int: with open(config_file) as f: orig_contents = contents = f.read() @@ -55,6 +63,7 @@ def migrate_config(config_file: str, quiet: bool = False) -> int: contents = _migrate_map(contents) contents = _migrate_sha_to_rev(contents) + contents = _migrate_python_venv(contents) if contents != orig_contents: with open(config_file, 'w') as f: diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 616faf54c..308e80c70 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -3,6 +3,7 @@ import json import logging import os +import shlex from typing import Any from typing import Sequence @@ -68,6 +69,14 @@ def _hook_install(hook: Hook) -> None: logger.info('Once installed this environment will be reused.') logger.info('This may take a few minutes...') + if hook.language == 'python_venv': + logger.warning( + f'`repo: {hook.src}` uses deprecated `language: python_venv`. ' + f'This is an alias for `language: python`. ' + f'Often `pre-commit autoupdate --repo {shlex.quote(hook.src)}` ' + f'will fix this.', + ) + lang = languages[hook.language] assert lang.ENVIRONMENT_DIR is not None diff --git a/testing/resources/python_venv_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/python_venv_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index a666ed87a..000000000 --- a/testing/resources/python_venv_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: foo - name: Foo - entry: foo - language: python_venv - files: \.py$ diff --git a/testing/resources/python_venv_hooks_repo/foo.py b/testing/resources/python_venv_hooks_repo/foo.py deleted file mode 100644 index 40efde392..000000000 --- a/testing/resources/python_venv_hooks_repo/foo.py +++ /dev/null @@ -1,9 +0,0 @@ -from __future__ import annotations - -import sys - - -def main(): - print(repr(sys.argv[1:])) - print('Hello World') - return 0 diff --git a/testing/resources/python_venv_hooks_repo/setup.py b/testing/resources/python_venv_hooks_repo/setup.py deleted file mode 100644 index cff6cadf3..000000000 --- a/testing/resources/python_venv_hooks_repo/setup.py +++ /dev/null @@ -1,10 +0,0 @@ -from __future__ import annotations - -from setuptools import setup - -setup( - name='foo', - version='0.0.0', - py_modules=['foo'], - entry_points={'console_scripts': ['foo = foo:main']}, -) diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index fca1ad92f..ba1846360 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -134,6 +134,39 @@ def test_migrate_config_sha_to_rev(tmpdir): ) +def test_migrate_config_language_python_venv(tmp_path): + src = '''\ +repos: +- repo: local + hooks: + - id: example + name: example + entry: example + language: python_venv + - id: example + name: example + entry: example + language: system +''' + expected = '''\ +repos: +- repo: local + hooks: + - id: example + name: example + entry: example + language: python + - id: example + name: example + entry: example + language: system +''' + cfg = tmp_path.joinpath('cfg.yaml') + cfg.write_text(src) + assert migrate_config(str(cfg)) == 0 + assert cfg.read_text() == expected + + def test_migrate_config_invalid_yaml(tmpdir): contents = '[' cfg = tmpdir.join(C.CONFIG_FILE) diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py new file mode 100644 index 000000000..33b8925fb --- /dev/null +++ b/tests/languages/all_test.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from pre_commit.languages.all import languages + + +def test_python_venv_is_an_alias_to_python(): + assert languages['python_venv'] is languages['python'] diff --git a/tests/repository_test.py b/tests/repository_test.py index b43b344c8..9ec2d5493 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -129,11 +129,21 @@ def test_python_hook_weird_setup_cfg(in_git_dir, tempdir_factory, store): ) -def test_python_venv(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'python_venv_hooks_repo', - 'foo', [os.devnull], - f'[{os.devnull!r}]\nHello World\n'.encode(), +def test_python_venv_deprecation(store, caplog): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'example', + 'name': 'example', + 'language': 'python_venv', + 'entry': 'echo hi', + }], + } + _get_hook(config, store, 'example') + assert caplog.messages[-1] == ( + '`repo: local` uses deprecated `language: python_venv`. ' + 'This is an alias for `language: python`. ' + 'Often `pre-commit autoupdate --repo local` will fix this.' ) From 0afb95ccca2f590bf45f45bcafb8ca792ce66423 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 4 Feb 2023 16:50:40 -0500 Subject: [PATCH 53/91] test docker and docker_image directly --- pre_commit/languages/docker.py | 3 +- testing/language_helpers.py | 7 ++-- .../docker_hooks_repo/.pre-commit-hooks.yaml | 17 -------- .../resources/docker_hooks_repo/Dockerfile | 3 -- .../.pre-commit-hooks.yaml | 8 ---- testing/util.py | 15 ------- tests/languages/docker_image_test.py | 27 +++++++++++++ tests/languages/docker_test.py | 14 +++++++ tests/repository_test.py | 40 ------------------- 9 files changed, 46 insertions(+), 88 deletions(-) delete mode 100644 testing/resources/docker_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/docker_hooks_repo/Dockerfile delete mode 100644 testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml create mode 100644 tests/languages/docker_image_test.py diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index e80c95978..2212c5ccb 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -138,9 +138,8 @@ def run_hook( entry_exe, *cmd_rest = helpers.hook_cmd(entry, args) entry_tag = ('--entrypoint', entry_exe, docker_tag(prefix)) - cmd = (*docker_cmd(), *entry_tag, *cmd_rest) return helpers.run_xargs( - cmd, + (*docker_cmd(), *entry_tag, *cmd_rest), file_args, require_serial=require_serial, color=color, diff --git a/testing/language_helpers.py b/testing/language_helpers.py index b9c538403..0964fbb44 100644 --- a/testing/language_helpers.py +++ b/testing/language_helpers.py @@ -20,9 +20,10 @@ def run_language( prefix = Prefix(str(path)) version = version or language.get_default_version() - language.install_environment(prefix, version, deps) - health_error = language.health_check(prefix, version) - assert health_error is None, health_error + if language.ENVIRONMENT_DIR is not None: + language.install_environment(prefix, version, deps) + health_error = language.health_check(prefix, version) + assert health_error is None, health_error with language.in_env(prefix, version): ret, out = language.run_hook( prefix, diff --git a/testing/resources/docker_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/docker_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 529573965..000000000 --- a/testing/resources/docker_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,17 +0,0 @@ -- id: docker-hook - name: Docker test hook - entry: echo - language: docker - files: \.txt$ - -- id: docker-hook-arg - name: Docker test hook - entry: echo -n - language: docker - files: \.txt$ - -- id: docker-hook-failing - name: Docker test hook with nonzero exit code - entry: bork - language: docker - files: \.txt$ diff --git a/testing/resources/docker_hooks_repo/Dockerfile b/testing/resources/docker_hooks_repo/Dockerfile deleted file mode 100644 index 0bd1de0cf..000000000 --- a/testing/resources/docker_hooks_repo/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM ubuntu:focal - -CMD ["echo", "This is overwritten by the .pre-commit-hooks.yaml 'entry'"] diff --git a/testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index e9fb24569..000000000 --- a/testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,8 +0,0 @@ -- id: echo-entrypoint - name: echo (via --entrypoint) - language: docker_image - entry: --entrypoint echo ubuntu:focal -- id: echo-cmd - name: echo (via cmd) - language: docker_image - entry: ubuntu:focal echo diff --git a/testing/util.py b/testing/util.py index b6c3804e6..7c68d0eee 100644 --- a/testing/util.py +++ b/testing/util.py @@ -6,24 +6,13 @@ import pytest -from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output -from pre_commit.util import cmd_output_b from testing.auto_namedtuple import auto_namedtuple TESTING_DIR = os.path.abspath(os.path.dirname(__file__)) -def docker_is_running() -> bool: # pragma: win32 no cover - try: - cmd_output_b('docker', 'ps') - except CalledProcessError: # pragma: no cover - return False - else: - return True - - def get_resource_path(path): return os.path.join(TESTING_DIR, 'resources', path) @@ -41,10 +30,6 @@ def cmd_output_mocked_pre_commit_home( return ret, out.replace('\r\n', '\n'), None -skipif_cant_run_docker = pytest.mark.skipif( - os.name == 'nt' or not docker_is_running(), - reason="Docker isn't running or can't be accessed", -) xfailif_windows = pytest.mark.xfail(os.name == 'nt', reason='windows') diff --git a/tests/languages/docker_image_test.py b/tests/languages/docker_image_test.py new file mode 100644 index 000000000..7993c11a8 --- /dev/null +++ b/tests/languages/docker_image_test.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from pre_commit.languages import docker_image +from testing.language_helpers import run_language +from testing.util import xfailif_windows + + +@xfailif_windows # pragma: win32 no cover +def test_docker_image_hook_via_entrypoint(tmp_path): + ret = run_language( + tmp_path, + docker_image, + '--entrypoint echo ubuntu:22.04', + args=('hello hello world',), + ) + assert ret == (0, b'hello hello world\n') + + +@xfailif_windows # pragma: win32 no cover +def test_docker_image_hook_via_args(tmp_path): + ret = run_language( + tmp_path, + docker_image, + 'ubuntu:22.04 echo', + args=('hello hello world',), + ) + assert ret == (0, b'hello hello world\n') diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 5f7c85e71..836382a8a 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -11,6 +11,8 @@ from pre_commit.languages import docker from pre_commit.util import CalledProcessError +from testing.language_helpers import run_language +from testing.util import xfailif_windows DOCKER_CGROUP_EXAMPLE = b'''\ 12:hugetlb:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 @@ -181,3 +183,15 @@ def test_get_docker_path_in_docker_docker_in_docker(in_docker): err = CalledProcessError(1, (), b'', b'') with mock.patch.object(docker, 'cmd_output_b', side_effect=err): assert docker._get_docker_path('/project') == '/project' + + +@xfailif_windows # pragma: win32 no cover +def test_docker_hook(tmp_path): + dockerfile = '''\ +FROM ubuntu:22.04 +CMD ["echo", "This is overwritten by the entry"'] +''' + tmp_path.joinpath('Dockerfile').write_text(dockerfile) + + ret = run_language(tmp_path, docker, 'echo hello hello world') + assert ret == (0, b'hello hello world\n') diff --git a/tests/repository_test.py b/tests/repository_test.py index 9ec2d5493..a4dcda5b3 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -30,7 +30,6 @@ from testing.fixtures import modify_manifest from testing.util import cwd from testing.util import get_resource_path -from testing.util import skipif_cant_run_docker def _norm_out(b): @@ -163,45 +162,6 @@ def test_language_versioned_python_hook(tempdir_factory, store): ) -@skipif_cant_run_docker # pragma: win32 no cover -def test_run_a_docker_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'docker_hooks_repo', - 'docker-hook', - ['Hello World from docker'], b'Hello World from docker\n', - ) - - -@skipif_cant_run_docker # pragma: win32 no cover -def test_run_a_docker_hook_with_entry_args(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'docker_hooks_repo', - 'docker-hook-arg', - ['Hello World from docker'], b'Hello World from docker', - ) - - -@skipif_cant_run_docker # pragma: win32 no cover -def test_run_a_failing_docker_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'docker_hooks_repo', - 'docker-hook-failing', - ['Hello World from docker'], - mock.ANY, # an error message about `bork` not existing - expected_return_code=127, - ) - - -@skipif_cant_run_docker # pragma: win32 no cover -@pytest.mark.parametrize('hook_id', ('echo-entrypoint', 'echo-cmd')) -def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): - _test_hook_repo( - tempdir_factory, store, 'docker_image_hooks_repo', - hook_id, - ['Hello World from docker'], b'Hello World from docker\n', - ) - - def test_system_hook_with_spaces(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'system_hook_with_spaces_repo', From 6804100701a40c7defdbd5027e459385ceeba8f2 Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Mon, 6 Feb 2023 12:24:30 -0600 Subject: [PATCH 54/91] test golang directly --- .../golang_hooks_repo/.pre-commit-hooks.yaml | 5 - testing/resources/golang_hooks_repo/go.mod | 5 - testing/resources/golang_hooks_repo/go.sum | 2 - .../golang-hello-world/main.go | 23 ---- tests/languages/golang_test.py | 93 +++++++++++++ tests/repository_test.py | 126 ------------------ tests/store_test.py | 24 ++++ 7 files changed, 117 insertions(+), 161 deletions(-) delete mode 100644 testing/resources/golang_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/golang_hooks_repo/go.mod delete mode 100644 testing/resources/golang_hooks_repo/go.sum delete mode 100644 testing/resources/golang_hooks_repo/golang-hello-world/main.go diff --git a/testing/resources/golang_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/golang_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 206733bb6..000000000 --- a/testing/resources/golang_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: golang-hook - name: golang example hook - entry: golang-hello-world - language: golang - files: '' diff --git a/testing/resources/golang_hooks_repo/go.mod b/testing/resources/golang_hooks_repo/go.mod deleted file mode 100644 index f37d4b674..000000000 --- a/testing/resources/golang_hooks_repo/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module golang-hello-world - -go 1.18 - -require github.com/BurntSushi/toml v1.1.0 diff --git a/testing/resources/golang_hooks_repo/go.sum b/testing/resources/golang_hooks_repo/go.sum deleted file mode 100644 index ec0c385a0..000000000 --- a/testing/resources/golang_hooks_repo/go.sum +++ /dev/null @@ -1,2 +0,0 @@ -github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= -github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= diff --git a/testing/resources/golang_hooks_repo/golang-hello-world/main.go b/testing/resources/golang_hooks_repo/golang-hello-world/main.go deleted file mode 100644 index 168574384..000000000 --- a/testing/resources/golang_hooks_repo/golang-hello-world/main.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - - -import ( - "fmt" - "runtime" - "github.com/BurntSushi/toml" - "os" -) - -type Config struct { - What string -} - -func main() { - message := runtime.Version() - if len(os.Args) > 1 { - message = os.Args[1] - } - var conf Config - toml.Decode("What = 'world'\n", &conf) - fmt.Printf("hello %v from %s\n", conf.What, message) -} diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py index 7c04255bc..f5f9985b8 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -6,8 +6,11 @@ import re_assert import pre_commit.constants as C +from pre_commit.envcontext import envcontext from pre_commit.languages import golang from pre_commit.languages import helpers +from pre_commit.store import _make_local_repo +from testing.language_helpers import run_language ACTUAL_GET_DEFAULT_VERSION = golang.get_default_version.__wrapped__ @@ -41,3 +44,93 @@ def test_golang_infer_go_version_default(): assert version != C.DEFAULT re_assert.Matches(r'^\d+\.\d+(?:\.\d+)?$').assert_matches(version) + + +def _make_hello_world(tmp_path): + go_mod = '''\ +module golang-hello-world + +go 1.18 + +require github.com/BurntSushi/toml v1.1.0 +''' + go_sum = '''\ +github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= +github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +''' # noqa: E501 + hello_world_go = '''\ +package main + + +import ( + "fmt" + "github.com/BurntSushi/toml" +) + +type Config struct { + What string +} + +func main() { + var conf Config + toml.Decode("What = 'world'\\n", &conf) + fmt.Printf("hello %v\\n", conf.What) +} +''' + tmp_path.joinpath('go.mod').write_text(go_mod) + tmp_path.joinpath('go.sum').write_text(go_sum) + mod_dir = tmp_path.joinpath('golang-hello-world') + mod_dir.mkdir() + main_file = mod_dir.joinpath('main.go') + main_file.write_text(hello_world_go) + + +def test_golang_system(tmp_path): + _make_hello_world(tmp_path) + + ret = run_language(tmp_path, golang, 'golang-hello-world') + assert ret == (0, b'hello world\n') + + +def test_golang_default_version(tmp_path): + _make_hello_world(tmp_path) + + ret = run_language( + tmp_path, + golang, + 'golang-hello-world', + version=C.DEFAULT, + ) + assert ret == (0, b'hello world\n') + + +def test_golang_versioned(tmp_path): + _make_local_repo(str(tmp_path)) + + ret, out = run_language( + tmp_path, + golang, + 'go version', + version='1.18.4', + ) + + assert ret == 0 + assert out.startswith(b'go version go1.18.4') + + +def test_local_golang_additional_deps(tmp_path): + _make_local_repo(str(tmp_path)) + + ret = run_language( + tmp_path, + golang, + 'hello', + deps=('golang.org/x/example/hello@latest',), + ) + + assert ret == (0, b'Hello, Go examples!\n') + + +def test_golang_hook_still_works_when_gobin_is_set(tmp_path): + with envcontext((('GOBIN', str(tmp_path.joinpath('gobin'))),)): + test_golang_system(tmp_path) diff --git a/tests/repository_test.py b/tests/repository_test.py index a4dcda5b3..0c9bba741 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -10,12 +10,9 @@ import re_assert import pre_commit.constants as C -from pre_commit import git from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest -from pre_commit.envcontext import envcontext from pre_commit.hook import Hook -from pre_commit.languages import golang from pre_commit.languages import helpers from pre_commit.languages import python from pre_commit.languages.all import languages @@ -169,92 +166,6 @@ def test_system_hook_with_spaces(tempdir_factory, store): ) -def test_golang_system_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'golang_hooks_repo', - 'golang-hook', ['system'], b'hello world from system\n', - config_kwargs={ - 'hooks': [{ - 'id': 'golang-hook', - 'language_version': 'system', - }], - }, - ) - - -def test_golang_versioned_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'golang_hooks_repo', - 'golang-hook', [], b'hello world from go1.18.4\n', - config_kwargs={ - 'hooks': [{ - 'id': 'golang-hook', - 'language_version': '1.18.4', - }], - }, - ) - - -def test_golang_hook_still_works_when_gobin_is_set(tempdir_factory, store): - gobin_dir = tempdir_factory.get() - with envcontext((('GOBIN', gobin_dir),)): - test_golang_system_hook(tempdir_factory, store) - assert os.listdir(gobin_dir) == [] - - -def test_golang_with_recursive_submodule(tmpdir, tempdir_factory, store): - sub_go = '''\ -package sub - -import "fmt" - -func Func() { - fmt.Println("hello hello world") -} -''' - sub = tmpdir.join('sub').ensure_dir() - sub.join('sub.go').write(sub_go) - cmd_output('git', '-C', str(sub), 'init', '.') - cmd_output('git', '-C', str(sub), 'add', '.') - git.commit(str(sub)) - - pre_commit_hooks = '''\ -- id: example - name: example - entry: example - language: golang - verbose: true -''' - go_mod = '''\ -module github.com/asottile/example - -go 1.14 -''' - main_go = '''\ -package main - -import "github.com/asottile/example/sub" - -func main() { - sub.Func() -} -''' - repo = tmpdir.join('repo').ensure_dir() - repo.join('.pre-commit-hooks.yaml').write(pre_commit_hooks) - repo.join('go.mod').write(go_mod) - repo.join('main.go').write(main_go) - cmd_output('git', '-C', str(repo), 'init', '.') - cmd_output('git', '-C', str(repo), 'add', '.') - cmd_output('git', '-C', str(repo), 'submodule', 'add', str(sub), 'sub') - git.commit(str(repo)) - - config = make_config_from_repo(str(repo)) - hook = _get_hook(config, store, 'example') - ret, out = _hook_run(hook, (), color=False) - assert ret == 0 - assert _norm_out(out) == b'hello hello world\n' - - def test_missing_executable(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'not_found_exe', @@ -419,43 +330,6 @@ def test_repository_state_compatibility(tempdir_factory, store, v): assert _hook_installed(hook) is True -def test_additional_golang_dependencies_installed( - tempdir_factory, store, -): - path = make_repo(tempdir_factory, 'golang_hooks_repo') - config = make_config_from_repo(path) - # A small go package - deps = ['golang.org/x/example/hello@latest'] - config['hooks'][0]['additional_dependencies'] = deps - hook = _get_hook(config, store, 'golang-hook') - envdir = helpers.environment_dir( - hook.prefix, - golang.ENVIRONMENT_DIR, - golang.get_default_version(), - ) - binaries = os.listdir(os.path.join(envdir, 'bin')) - # normalize for windows - binaries = [os.path.splitext(binary)[0] for binary in binaries] - assert 'hello' in binaries - - -def test_local_golang_additional_dependencies(store): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'hello', - 'name': 'hello', - 'entry': 'hello', - 'language': 'golang', - 'additional_dependencies': ['golang.org/x/example/hello@latest'], - }], - } - hook = _get_hook(config, store, 'hello') - ret, out = _hook_run(hook, (), color=False) - assert ret == 0 - assert _norm_out(out) == b'Hello, Go examples!\n' - - def test_fail_hooks(store): config = { 'repo': 'local', diff --git a/tests/store_test.py b/tests/store_test.py index c42ce6537..146eac416 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -246,3 +246,27 @@ def _chmod_minus_w(p): # should be skipped due to readonly store.mark_config_used(str(cfg)) assert store.select_all_configs() == [] + + +def test_clone_with_recursive_submodules(store, tmp_path): + sub = tmp_path.joinpath('sub') + sub.mkdir() + sub.joinpath('submodule').write_text('i am a submodule') + cmd_output('git', '-C', str(sub), 'init', '.') + cmd_output('git', '-C', str(sub), 'add', '.') + git.commit(str(sub)) + + repo = tmp_path.joinpath('repo') + repo.mkdir() + repo.joinpath('repository').write_text('i am a repo') + cmd_output('git', '-C', str(repo), 'init', '.') + cmd_output('git', '-C', str(repo), 'add', '.') + cmd_output('git', '-C', str(repo), 'submodule', 'add', str(sub), 'sub') + git.commit(str(repo)) + + rev = git.head_rev(str(repo)) + ret = store.clone(str(repo), rev) + + assert os.path.exists(ret) + assert os.path.exists(os.path.join(ret, str(repo), 'repository')) + assert os.path.exists(os.path.join(ret, str(sub), 'submodule')) From 915b930a5d0c894a4b0d2a6957f833179255cd42 Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Tue, 7 Feb 2023 21:22:26 -0600 Subject: [PATCH 55/91] test dotnet directly --- pre_commit/languages/dotnet.py | 4 - .../.pre-commit-hooks.yaml | 12 -- .../dotnet_hooks_combo_repo.sln | 28 ---- .../dotnet_hooks_combo_repo/proj1/Program.cs | 12 -- .../proj1/proj1.csproj | 12 -- .../dotnet_hooks_combo_repo/proj2/Program.cs | 12 -- .../proj2/proj2.csproj | 12 -- .../.gitignore | 3 - .../.pre-commit-hooks.yaml | 5 - .../Program.cs | 12 -- .../dotnet_hooks_csproj_prefix_repo.csproj | 9 - .../dotnet_hooks_csproj_repo/.gitignore | 3 - .../.pre-commit-hooks.yaml | 5 - .../dotnet_hooks_csproj_repo/Program.cs | 12 -- .../dotnet_hooks_csproj_repo.csproj | 9 - .../dotnet_hooks_sln_repo/.gitignore | 3 - .../.pre-commit-hooks.yaml | 5 - .../dotnet_hooks_sln_repo/Program.cs | 12 -- .../dotnet_hooks_sln_repo.csproj | 9 - .../dotnet_hooks_sln_repo.sln | 34 ---- tests/languages/dotnet_test.py | 154 ++++++++++++++++++ tests/repository_test.py | 16 -- 22 files changed, 154 insertions(+), 229 deletions(-) delete mode 100644 testing/resources/dotnet_hooks_combo_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/dotnet_hooks_combo_repo/dotnet_hooks_combo_repo.sln delete mode 100644 testing/resources/dotnet_hooks_combo_repo/proj1/Program.cs delete mode 100644 testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj delete mode 100644 testing/resources/dotnet_hooks_combo_repo/proj2/Program.cs delete mode 100644 testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj delete mode 100644 testing/resources/dotnet_hooks_csproj_prefix_repo/.gitignore delete mode 100644 testing/resources/dotnet_hooks_csproj_prefix_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/dotnet_hooks_csproj_prefix_repo/Program.cs delete mode 100644 testing/resources/dotnet_hooks_csproj_prefix_repo/dotnet_hooks_csproj_prefix_repo.csproj delete mode 100644 testing/resources/dotnet_hooks_csproj_repo/.gitignore delete mode 100644 testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/dotnet_hooks_csproj_repo/Program.cs delete mode 100644 testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj delete mode 100644 testing/resources/dotnet_hooks_sln_repo/.gitignore delete mode 100644 testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/dotnet_hooks_sln_repo/Program.cs delete mode 100644 testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj delete mode 100644 testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.sln diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index 4c3955e85..05d4ce32c 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -109,7 +109,3 @@ def install_environment( tool_id, ), ) - - # Clean the git dir, ignoring the environment dir - clean_cmd = ('git', 'clean', '-ffxd', '-e', f'{ENVIRONMENT_DIR}-*') - helpers.run_setup_cmd(prefix, clean_cmd) diff --git a/testing/resources/dotnet_hooks_combo_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_combo_repo/.pre-commit-hooks.yaml deleted file mode 100644 index f221854a4..000000000 --- a/testing/resources/dotnet_hooks_combo_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,12 +0,0 @@ -- id: dotnet-example-hook - name: Test Project 1 - description: Test Project 1 - entry: proj1 - language: dotnet - stages: [commit] -- id: proj2 - name: Test Project 2 - description: Test Project 2 - entry: proj2 - language: dotnet - stages: [commit] diff --git a/testing/resources/dotnet_hooks_combo_repo/dotnet_hooks_combo_repo.sln b/testing/resources/dotnet_hooks_combo_repo/dotnet_hooks_combo_repo.sln deleted file mode 100644 index edb0fcbc5..000000000 --- a/testing/resources/dotnet_hooks_combo_repo/dotnet_hooks_combo_repo.sln +++ /dev/null @@ -1,28 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30114.105 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "proj1", "proj1\proj1.csproj", "{38A939C3-DEA4-47D7-9B75-0418C4249662}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "proj2", "proj2\proj2.csproj", "{4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {38A939C3-DEA4-47D7-9B75-0418C4249662}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {38A939C3-DEA4-47D7-9B75-0418C4249662}.Debug|Any CPU.Build.0 = Debug|Any CPU - {38A939C3-DEA4-47D7-9B75-0418C4249662}.Release|Any CPU.ActiveCfg = Release|Any CPU - {38A939C3-DEA4-47D7-9B75-0418C4249662}.Release|Any CPU.Build.0 = Release|Any CPU - {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal diff --git a/testing/resources/dotnet_hooks_combo_repo/proj1/Program.cs b/testing/resources/dotnet_hooks_combo_repo/proj1/Program.cs deleted file mode 100644 index 03876f5cd..000000000 --- a/testing/resources/dotnet_hooks_combo_repo/proj1/Program.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace proj1 -{ - class Program - { - static void Main(string[] args) - { - Console.Write("Hello from dotnet!\n"); - } - } -} diff --git a/testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj b/testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj deleted file mode 100644 index 861ced6d9..000000000 --- a/testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - Exe - net6 - - true - proj1 - ./nupkg - - - diff --git a/testing/resources/dotnet_hooks_combo_repo/proj2/Program.cs b/testing/resources/dotnet_hooks_combo_repo/proj2/Program.cs deleted file mode 100644 index 47a99a358..000000000 --- a/testing/resources/dotnet_hooks_combo_repo/proj2/Program.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace proj2 -{ - class Program - { - static void Main(string[] args) - { - Console.WriteLine("Hello World!"); - } - } -} diff --git a/testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj b/testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj deleted file mode 100644 index dfce2cad1..000000000 --- a/testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - Exe - net6 - - true - proj2 - ./nupkg - - - diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/.gitignore b/testing/resources/dotnet_hooks_csproj_prefix_repo/.gitignore deleted file mode 100644 index edcd28f4a..000000000 --- a/testing/resources/dotnet_hooks_csproj_prefix_repo/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -bin/ -obj/ -nupkg/ diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_csproj_prefix_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 6626627d7..000000000 --- a/testing/resources/dotnet_hooks_csproj_prefix_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: dotnet-example-hook - name: dotnet example hook - entry: testeroni.tool - language: dotnet - files: '' diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/Program.cs b/testing/resources/dotnet_hooks_csproj_prefix_repo/Program.cs deleted file mode 100644 index 1456e8ef2..000000000 --- a/testing/resources/dotnet_hooks_csproj_prefix_repo/Program.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace dotnet_hooks_repo -{ - class Program - { - static void Main(string[] args) - { - Console.WriteLine("Hello from dotnet!"); - } - } -} diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/dotnet_hooks_csproj_prefix_repo.csproj b/testing/resources/dotnet_hooks_csproj_prefix_repo/dotnet_hooks_csproj_prefix_repo.csproj deleted file mode 100644 index 754b76006..000000000 --- a/testing/resources/dotnet_hooks_csproj_prefix_repo/dotnet_hooks_csproj_prefix_repo.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - Exe - net7.0 - true - testeroni.tool - ./nupkg - - diff --git a/testing/resources/dotnet_hooks_csproj_repo/.gitignore b/testing/resources/dotnet_hooks_csproj_repo/.gitignore deleted file mode 100644 index edcd28f4a..000000000 --- a/testing/resources/dotnet_hooks_csproj_repo/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -bin/ -obj/ -nupkg/ diff --git a/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 0f514c116..000000000 --- a/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: dotnet-example-hook - name: dotnet example hook - entry: testeroni - language: dotnet - files: '' diff --git a/testing/resources/dotnet_hooks_csproj_repo/Program.cs b/testing/resources/dotnet_hooks_csproj_repo/Program.cs deleted file mode 100644 index 1456e8ef2..000000000 --- a/testing/resources/dotnet_hooks_csproj_repo/Program.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace dotnet_hooks_repo -{ - class Program - { - static void Main(string[] args) - { - Console.WriteLine("Hello from dotnet!"); - } - } -} diff --git a/testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj b/testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj deleted file mode 100644 index fa9879b0d..000000000 --- a/testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - Exe - net6 - true - testeroni - ./nupkg - - diff --git a/testing/resources/dotnet_hooks_sln_repo/.gitignore b/testing/resources/dotnet_hooks_sln_repo/.gitignore deleted file mode 100644 index edcd28f4a..000000000 --- a/testing/resources/dotnet_hooks_sln_repo/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -bin/ -obj/ -nupkg/ diff --git a/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 0f514c116..000000000 --- a/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: dotnet-example-hook - name: dotnet example hook - entry: testeroni - language: dotnet - files: '' diff --git a/testing/resources/dotnet_hooks_sln_repo/Program.cs b/testing/resources/dotnet_hooks_sln_repo/Program.cs deleted file mode 100644 index 04ad4e0cc..000000000 --- a/testing/resources/dotnet_hooks_sln_repo/Program.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace dotnet_hooks_sln_repo -{ - class Program - { - static void Main(string[] args) - { - Console.WriteLine("Hello from dotnet!"); - } - } -} diff --git a/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj b/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj deleted file mode 100644 index a4e2d0058..000000000 --- a/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - Exe - net6 - true - testeroni - ./nupkg - - diff --git a/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.sln b/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.sln deleted file mode 100644 index 87d2afbaf..000000000 --- a/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.sln +++ /dev/null @@ -1,34 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26124.0 -MinimumVisualStudioVersion = 15.0.26124.0 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet_hooks_sln_repo", "dotnet_hooks_sln_repo.csproj", "{6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x64.ActiveCfg = Debug|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x64.Build.0 = Debug|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x86.ActiveCfg = Debug|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x86.Build.0 = Debug|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|Any CPU.Build.0 = Release|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x64.ActiveCfg = Release|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x64.Build.0 = Release|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x86.ActiveCfg = Release|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal diff --git a/tests/languages/dotnet_test.py b/tests/languages/dotnet_test.py index e69de29bb..470c03b22 100644 --- a/tests/languages/dotnet_test.py +++ b/tests/languages/dotnet_test.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +from pre_commit.languages import dotnet +from testing.language_helpers import run_language + + +def _write_program_cs(tmp_path): + program_cs = '''\ +using System; + +namespace dotnet_tests +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello from dotnet!"); + } + } +} +''' + tmp_path.joinpath('Program.cs').write_text(program_cs) + + +def _csproj(tool_name): + return f'''\ + + + Exe + net6 + true + {tool_name} + ./nupkg + + +''' + + +def test_dotnet_csproj(tmp_path): + csproj = _csproj('testeroni') + _write_program_cs(tmp_path) + tmp_path.joinpath('dotnet_csproj.csproj').write_text(csproj) + ret = run_language(tmp_path, dotnet, 'testeroni') + assert ret == (0, b'Hello from dotnet!\n') + + +def test_dotnet_csproj_prefix(tmp_path): + csproj = _csproj('testeroni.tool') + _write_program_cs(tmp_path) + tmp_path.joinpath('dotnet_hooks_csproj_prefix.csproj').write_text(csproj) + ret = run_language(tmp_path, dotnet, 'testeroni.tool') + assert ret == (0, b'Hello from dotnet!\n') + + +def test_dotnet_sln(tmp_path): + csproj = _csproj('testeroni') + sln = '''\ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet_hooks_sln_repo", "dotnet_hooks_sln_repo.csproj", "{6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x64.ActiveCfg = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x64.Build.0 = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x86.ActiveCfg = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x86.Build.0 = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|Any CPU.Build.0 = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x64.ActiveCfg = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x64.Build.0 = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x86.ActiveCfg = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal +''' # noqa: E501 + _write_program_cs(tmp_path) + tmp_path.joinpath('dotnet_hooks_sln_repo.csproj').write_text(csproj) + tmp_path.joinpath('dotnet_hooks_sln_repo.sln').write_text(sln) + + ret = run_language(tmp_path, dotnet, 'testeroni') + assert ret == (0, b'Hello from dotnet!\n') + + +def _setup_dotnet_combo(tmp_path): + sln = '''\ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "proj1", "proj1\\proj1.csproj", "{38A939C3-DEA4-47D7-9B75-0418C4249662}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "proj2", "proj2\\proj2.csproj", "{4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Release|Any CPU.Build.0 = Release|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal +''' # noqa: E501 + tmp_path.joinpath('dotnet_hooks_combo_repo.sln').write_text(sln) + + csproj1 = _csproj('proj1') + proj1 = tmp_path.joinpath('proj1') + proj1.mkdir() + proj1.joinpath('proj1.csproj').write_text(csproj1) + _write_program_cs(proj1) + + csproj2 = _csproj('proj2') + proj2 = tmp_path.joinpath('proj2') + proj2.mkdir() + proj2.joinpath('proj2.csproj').write_text(csproj2) + _write_program_cs(proj2) + + +def test_dotnet_combo_proj1(tmp_path): + _setup_dotnet_combo(tmp_path) + ret = run_language(tmp_path, dotnet, 'proj1') + assert ret == (0, b'Hello from dotnet!\n') + + +def test_dotnet_combo_proj2(tmp_path): + _setup_dotnet_combo(tmp_path) + ret = run_language(tmp_path, dotnet, 'proj2') + assert ret == (0, b'Hello from dotnet!\n') diff --git a/tests/repository_test.py b/tests/repository_test.py index 0c9bba741..9e2f1e519 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -625,22 +625,6 @@ def test_manifest_hooks(tempdir_factory, store): ) -@pytest.mark.parametrize( - 'repo', - ( - 'dotnet_hooks_csproj_repo', - 'dotnet_hooks_sln_repo', - 'dotnet_hooks_combo_repo', - 'dotnet_hooks_csproj_prefix_repo', - ), -) -def test_dotnet_hook(tempdir_factory, store, repo): - _test_hook_repo( - tempdir_factory, store, repo, - 'dotnet-example-hook', [], b'Hello from dotnet!\n', - ) - - def test_non_installable_hook_error_for_language_version(store, caplog): config = { 'repo': 'local', From abbfb2e9b9195f6ae03441a0d69e4d2f8575d416 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 8 Feb 2023 06:43:04 +0000 Subject: [PATCH 56/91] List golang as first-class language --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9bcb79ed..ab3a92989 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,10 +64,10 @@ to implement. The current implemented languages are at varying levels: - 0th class - pre-commit does not require any dependencies for these languages as they're not actually languages (current examples: fail, pygrep) - 1st class - pre-commit will bootstrap a full interpreter requiring nothing to - be installed globally (current examples: node, ruby, rust) + be installed globally (current examples: go, node, ruby, rust) - 2nd class - pre-commit requires the user to install the language globally but - will install tools in an isolated fashion (current examples: python, go, - swift, docker). + will install tools in an isolated fashion (current examples: python, swift, + docker). - 3rd class - pre-commit requires the user to install both the tool and the language globally (current examples: script, system) From 563507937324d8214a82f3cfd6199ea4ace875d0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 8 Feb 2023 11:20:30 -0500 Subject: [PATCH 57/91] force the issue template more --- .../ISSUE_TEMPLATE/{bug.yaml => 00_bug.yaml} | 6 +++ .github/ISSUE_TEMPLATE/01_feature.yaml | 38 +++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yaml | 6 +++ 3 files changed, 50 insertions(+) rename .github/ISSUE_TEMPLATE/{bug.yaml => 00_bug.yaml} (87%) create mode 100644 .github/ISSUE_TEMPLATE/01_feature.yaml create mode 100644 .github/ISSUE_TEMPLATE/config.yaml diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/00_bug.yaml similarity index 87% rename from .github/ISSUE_TEMPLATE/bug.yaml rename to .github/ISSUE_TEMPLATE/00_bug.yaml index 96cd6c75c..980f7afee 100644 --- a/.github/ISSUE_TEMPLATE/bug.yaml +++ b/.github/ISSUE_TEMPLATE/00_bug.yaml @@ -16,6 +16,12 @@ body: placeholder: ... validations: required: true + - type: markdown + attributes: + value: | + 95% of issues created are duplicates. + please try extra hard to find them first. + it's very unlikely your problem is unique. - type: textarea id: freeform attributes: diff --git a/.github/ISSUE_TEMPLATE/01_feature.yaml b/.github/ISSUE_TEMPLATE/01_feature.yaml new file mode 100644 index 000000000..c7ddc84cd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01_feature.yaml @@ -0,0 +1,38 @@ +name: feature request +description: something new +body: + - type: markdown + attributes: + value: | + this is for issues for `pre-commit` (the framework). + if you are reporting an issue for [pre-commit.ci] please report it at [pre-commit-ci/issues] + + [pre-commit.ci]: https://pre-commit.ci + [pre-commit-ci/issues]: https://github.com/pre-commit-ci/issues + - type: input + id: search + attributes: + label: search you tried in the issue tracker + placeholder: ... + validations: + required: true + - type: markdown + attributes: + value: | + 95% of issues created are duplicates. + please try extra hard to find them first. + it's very unlikely your feature idea is a new one. + - type: textarea + id: freeform + attributes: + label: describe your actual problem + placeholder: 'I want to do ... I tried ... It does not work because ...' + validations: + required: true + - type: input + id: version + attributes: + label: pre-commit --version + placeholder: pre-commit x.x.x + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yaml new file mode 100644 index 000000000..a2d14826c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yaml @@ -0,0 +1,6 @@ +blank_issues_enabled: false +contact_links: +- name: documentation + url: https://pre-commit.com +- name: pre-commit.ci issues + url: https://github.com/pre-commit-ci/issues From 16869444cae5ebea1917a2442e37eff381c44c76 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 8 Feb 2023 11:21:50 -0500 Subject: [PATCH 58/91] git mv .github/ISSUE_TEMPLATE/config.{yaml,yml} --- .github/ISSUE_TEMPLATE/{config.yaml => config.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/ISSUE_TEMPLATE/{config.yaml => config.yml} (100%) diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/config.yaml rename to .github/ISSUE_TEMPLATE/config.yml From 4bd1677cda652a92c38a6051e7b8a1d76e36364b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 8 Feb 2023 11:23:43 -0500 Subject: [PATCH 59/91] do template links need about? --- .github/ISSUE_TEMPLATE/config.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index a2d14826c..4179f47f3 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,5 +2,7 @@ blank_issues_enabled: false contact_links: - name: documentation url: https://pre-commit.com + about: please check the docs first - name: pre-commit.ci issues url: https://github.com/pre-commit-ci/issues + about: please report issues about pre-commit.ci here From 4fdfb25a5245e63dd424f72ef7f66dfe49b2b53b Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Fri, 10 Feb 2023 16:18:43 -0600 Subject: [PATCH 60/91] test fail language inline --- tests/languages/fail_test.py | 14 ++++++++++++++ tests/repository_test.py | 24 ------------------------ 2 files changed, 14 insertions(+), 24 deletions(-) create mode 100644 tests/languages/fail_test.py diff --git a/tests/languages/fail_test.py b/tests/languages/fail_test.py new file mode 100644 index 000000000..7c74886fd --- /dev/null +++ b/tests/languages/fail_test.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from pre_commit.languages import fail +from testing.language_helpers import run_language + + +def test_fail_hooks(tmp_path): + ret = run_language( + tmp_path, + fail, + 'watch out for', + file_args=('bunnies',), + ) + assert ret == (1, b'watch out for\n\nbunnies\n') diff --git a/tests/repository_test.py b/tests/repository_test.py index 9e2f1e519..1a16e691f 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -330,30 +330,6 @@ def test_repository_state_compatibility(tempdir_factory, store, v): assert _hook_installed(hook) is True -def test_fail_hooks(store): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'fail', - 'name': 'fail', - 'language': 'fail', - 'entry': 'make sure to name changelogs as .rst!', - 'files': r'changelog/.*(? Date: Tue, 14 Feb 2023 02:25:01 +0000 Subject: [PATCH 61/91] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.991 → v1.0.0](https://github.com/pre-commit/mirrors-mypy/compare/v0.991...v1.0.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b7d7f1f0d..023f4f683 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.991 + rev: v1.0.0 hooks: - id: mypy additional_dependencies: [types-all] From 8db5aaf4f32f9ac3d4407f70478d0aa15c0d4680 Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Fri, 17 Feb 2023 21:30:46 -0600 Subject: [PATCH 62/91] future-proof dotnet build command see https://github.com/dotnet/sdk/issues/30624#issuecomment-1435457318 --- pre_commit/languages/dotnet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index 05d4ce32c..3db2679d3 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -61,7 +61,7 @@ def install_environment( helpers.assert_no_additional_deps('dotnet', additional_dependencies) envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) - build_dir = 'pre-commit-build' + build_dir = prefix.path('pre-commit-build') # Build & pack nupkg file helpers.run_setup_cmd( @@ -69,7 +69,7 @@ def install_environment( ( 'dotnet', 'pack', '--configuration', 'Release', - '--output', build_dir, + '--property', f'PackageOutputPath={build_dir}', ), ) From a2373d0a8198425785951cbd5f037d9815abb2ab Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Wed, 15 Feb 2023 20:50:19 -0600 Subject: [PATCH 63/91] test pygrep inline --- tests/languages/pygrep_test.py | 17 +++++++++++++ tests/repository_test.py | 46 ---------------------------------- 2 files changed, 17 insertions(+), 46 deletions(-) diff --git a/tests/languages/pygrep_test.py b/tests/languages/pygrep_test.py index 8420046c5..c6271c807 100644 --- a/tests/languages/pygrep_test.py +++ b/tests/languages/pygrep_test.py @@ -3,6 +3,7 @@ import pytest from pre_commit.languages import pygrep +from testing.language_helpers import run_language @pytest.fixture @@ -13,6 +14,9 @@ def some_files(tmpdir): tmpdir.join('f4').write_binary(b'foo\npattern\nbar\n') tmpdir.join('f5').write_binary(b'[INFO] hi\npattern\nbar') tmpdir.join('f6').write_binary(b"pattern\nbarwith'foo\n") + tmpdir.join('f7').write_binary(b"hello'hi\nworld\n") + tmpdir.join('f8').write_binary(b'foo\nbar\nbaz\n') + tmpdir.join('f9').write_binary(b'[WARN] hi\n') with tmpdir.as_cwd(): yield @@ -125,3 +129,16 @@ def test_multiline_multiline_flag_is_enabled(cap_out): out = cap_out.get() assert ret == 1 assert out == 'f1:1:foo\nbar\n' + + +def test_grep_hook_matching(some_files, tmp_path): + ret = run_language( + tmp_path, pygrep, 'ello', file_args=('f7', 'f8', 'f9'), + ) + assert ret == (1, b"f7:1:hello'hi\n") + + +@pytest.mark.parametrize('regex', ('nope', "foo'bar", r'^\[INFO\]')) +def test_grep_hook_not_matching(regex, some_files, tmp_path): + ret = run_language(tmp_path, pygrep, regex, file_args=('f7', 'f8', 'f9')) + assert ret == (0, b'') diff --git a/tests/repository_test.py b/tests/repository_test.py index 1a16e691f..332816d25 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -226,52 +226,6 @@ def test_output_isatty(tempdir_factory, store): ) -def _make_grep_repo(entry, store, args=()): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'grep-hook', - 'name': 'grep-hook', - 'language': 'pygrep', - 'entry': entry, - 'args': args, - 'types': ['text'], - }], - } - return _get_hook(config, store, 'grep-hook') - - -@pytest.fixture -def greppable_files(tmpdir): - with tmpdir.as_cwd(): - cmd_output_b('git', 'init', '.') - tmpdir.join('f1').write_binary(b"hello'hi\nworld\n") - tmpdir.join('f2').write_binary(b'foo\nbar\nbaz\n') - tmpdir.join('f3').write_binary(b'[WARN] hi\n') - yield tmpdir - - -def test_grep_hook_matching(greppable_files, store): - hook = _make_grep_repo('ello', store) - ret, out = _hook_run(hook, ('f1', 'f2', 'f3'), color=False) - assert ret == 1 - assert _norm_out(out) == b"f1:1:hello'hi\n" - - -def test_grep_hook_case_insensitive(greppable_files, store): - hook = _make_grep_repo('ELLO', store, args=['-i']) - ret, out = _hook_run(hook, ('f1', 'f2', 'f3'), color=False) - assert ret == 1 - assert _norm_out(out) == b"f1:1:hello'hi\n" - - -@pytest.mark.parametrize('regex', ('nope', "foo'bar", r'^\[INFO\]')) -def test_grep_hook_not_matching(regex, greppable_files, store): - hook = _make_grep_repo(regex, store) - ret, out = _hook_run(hook, ('f1', 'f2', 'f3'), color=False) - assert (ret, out) == (0, b'') - - def _norm_pwd(path): # Under windows bash's temp and windows temp is different. # This normalizes to the bash /tmp From d3883ce7f77f6cb88b622326de23c09cf8552cf6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Feb 2023 17:59:15 -0500 Subject: [PATCH 64/91] move languages.all and languages.helpers out of languages --- pre_commit/all_languages.py | 48 +++++++++ pre_commit/clientlib.py | 8 +- pre_commit/commands/run.py | 2 +- .../{languages/helpers.py => lang_base.py} | 45 ++++++++- pre_commit/languages/all.py | 99 ------------------- pre_commit/languages/conda.py | 14 +-- pre_commit/languages/coursier.py | 18 ++-- pre_commit/languages/dart.py | 20 ++-- pre_commit/languages/docker.py | 20 ++-- pre_commit/languages/docker_image.py | 14 +-- pre_commit/languages/dotnet.py | 20 ++-- pre_commit/languages/fail.py | 10 +- pre_commit/languages/golang.py | 16 +-- pre_commit/languages/lua.py | 18 ++-- pre_commit/languages/node.py | 18 ++-- pre_commit/languages/perl.py | 14 +-- pre_commit/languages/pygrep.py | 10 +- pre_commit/languages/python.py | 14 +-- pre_commit/languages/r.py | 12 +-- pre_commit/languages/ruby.py | 24 ++--- pre_commit/languages/rust.py | 12 +-- pre_commit/languages/script.py | 14 +-- pre_commit/languages/swift.py | 16 +-- pre_commit/languages/system.py | 12 +-- pre_commit/repository.py | 4 +- testing/language_helpers.py | 2 +- .../all_test.py => all_languages_test.py} | 2 +- .../helpers_test.py => lang_base_test.py} | 34 +++---- tests/languages/golang_test.py | 4 +- tests/repository_test.py | 12 +-- 30 files changed, 274 insertions(+), 282 deletions(-) create mode 100644 pre_commit/all_languages.py rename pre_commit/{languages/helpers.py => lang_base.py} (75%) delete mode 100644 pre_commit/languages/all.py rename tests/{languages/all_test.py => all_languages_test.py} (75%) rename tests/{languages/helpers_test.py => lang_base_test.py} (78%) diff --git a/pre_commit/all_languages.py b/pre_commit/all_languages.py new file mode 100644 index 000000000..2bed7067f --- /dev/null +++ b/pre_commit/all_languages.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from pre_commit.lang_base import Language +from pre_commit.languages import conda +from pre_commit.languages import coursier +from pre_commit.languages import dart +from pre_commit.languages import docker +from pre_commit.languages import docker_image +from pre_commit.languages import dotnet +from pre_commit.languages import fail +from pre_commit.languages import golang +from pre_commit.languages import lua +from pre_commit.languages import node +from pre_commit.languages import perl +from pre_commit.languages import pygrep +from pre_commit.languages import python +from pre_commit.languages import r +from pre_commit.languages import ruby +from pre_commit.languages import rust +from pre_commit.languages import script +from pre_commit.languages import swift +from pre_commit.languages import system + + +languages: dict[str, Language] = { + 'conda': conda, + 'coursier': coursier, + 'dart': dart, + 'docker': docker, + 'docker_image': docker_image, + 'dotnet': dotnet, + 'fail': fail, + 'golang': golang, + 'lua': lua, + 'node': node, + 'perl': perl, + 'pygrep': pygrep, + 'python': python, + 'r': r, + 'ruby': ruby, + 'rust': rust, + 'script': script, + 'swift': swift, + 'system': system, + # TODO: fully deprecate `python_venv` + 'python_venv': python, +} +language_names = sorted(languages) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index e191d3a00..9ff38c6a2 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -12,8 +12,8 @@ from identify.identify import ALL_TAGS import pre_commit.constants as C +from pre_commit.all_languages import language_names from pre_commit.errors import FatalError -from pre_commit.languages.all import all_languages from pre_commit.yaml import yaml_load logger = logging.getLogger('pre_commit') @@ -49,7 +49,7 @@ def check_min_version(version: str) -> None: cfgv.Required('id', cfgv.check_string), cfgv.Required('name', cfgv.check_string), cfgv.Required('entry', cfgv.check_string), - cfgv.Required('language', cfgv.check_one_of(all_languages)), + cfgv.Required('language', cfgv.check_one_of(language_names)), cfgv.Optional('alias', cfgv.check_string, ''), cfgv.Optional('files', check_string_regex, ''), @@ -281,8 +281,8 @@ def check(self, dct: dict[str, Any]) -> None: ) DEFAULT_LANGUAGE_VERSION = cfgv.Map( 'DefaultLanguageVersion', None, - cfgv.NoAdditionalKeys(all_languages), - *(cfgv.Optional(x, cfgv.check_string, C.DEFAULT) for x in all_languages), + cfgv.NoAdditionalKeys(language_names), + *(cfgv.Optional(x, cfgv.check_string, C.DEFAULT) for x in language_names), ) CONFIG_SCHEMA = cfgv.Map( 'Config', None, diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index a7eb4f45a..c9bc55b42 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -19,9 +19,9 @@ from pre_commit import color from pre_commit import git from pre_commit import output +from pre_commit.all_languages import languages from pre_commit.clientlib import load_config from pre_commit.hook import Hook -from pre_commit.languages.all import languages from pre_commit.repository import all_hooks from pre_commit.repository import install_hook_envs from pre_commit.staged_files_only import staged_files_only diff --git a/pre_commit/languages/helpers.py b/pre_commit/lang_base.py similarity index 75% rename from pre_commit/languages/helpers.py rename to pre_commit/lang_base.py index d1be409c8..6ba412f0e 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/lang_base.py @@ -7,8 +7,10 @@ import re import shlex from typing import Any +from typing import ContextManager from typing import Generator from typing import NoReturn +from typing import Protocol from typing import Sequence import pre_commit.constants as C @@ -22,6 +24,47 @@ SHIMS_RE = re.compile(r'[/\\]shims[/\\]') +class Language(Protocol): + # Use `None` for no installation / environment + @property + def ENVIRONMENT_DIR(self) -> str | None: ... + # return a value to replace `'default` for `language_version` + def get_default_version(self) -> str: ... + # return whether the environment is healthy (or should be rebuilt) + def health_check(self, prefix: Prefix, version: str) -> str | None: ... + + # install a repository for the given language and language_version + def install_environment( + self, + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], + ) -> None: + ... + + # modify the environment for hook execution + def in_env( + self, + prefix: Prefix, + version: str, + ) -> ContextManager[None]: + ... + + # execute a hook and return the exit code and output + def run_hook( + self, + prefix: Prefix, + entry: str, + args: Sequence[str], + file_args: Sequence[str], + *, + is_local: bool, + require_serial: bool, + color: bool, + ) -> tuple[int, bytes]: + ... + + def exe_exists(exe: str) -> bool: found = parse_shebang.find_executable(exe) if found is None: # exe exists @@ -45,7 +88,7 @@ def exe_exists(exe: str) -> bool: ) -def run_setup_cmd(prefix: Prefix, cmd: tuple[str, ...], **kwargs: Any) -> None: +def setup_cmd(prefix: Prefix, cmd: tuple[str, ...], **kwargs: Any) -> None: cmd_output_b(*cmd, cwd=prefix.prefix_dir, **kwargs) diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py deleted file mode 100644 index d952ae1ab..000000000 --- a/pre_commit/languages/all.py +++ /dev/null @@ -1,99 +0,0 @@ -from __future__ import annotations - -from typing import ContextManager -from typing import Protocol -from typing import Sequence - -from pre_commit.languages import conda -from pre_commit.languages import coursier -from pre_commit.languages import dart -from pre_commit.languages import docker -from pre_commit.languages import docker_image -from pre_commit.languages import dotnet -from pre_commit.languages import fail -from pre_commit.languages import golang -from pre_commit.languages import lua -from pre_commit.languages import node -from pre_commit.languages import perl -from pre_commit.languages import pygrep -from pre_commit.languages import python -from pre_commit.languages import r -from pre_commit.languages import ruby -from pre_commit.languages import rust -from pre_commit.languages import script -from pre_commit.languages import swift -from pre_commit.languages import system -from pre_commit.prefix import Prefix - - -class Language(Protocol): - # Use `None` for no installation / environment - @property - def ENVIRONMENT_DIR(self) -> str | None: ... - # return a value to replace `'default` for `language_version` - def get_default_version(self) -> str: ... - - # return whether the environment is healthy (or should be rebuilt) - def health_check( - self, - prefix: Prefix, - language_version: str, - ) -> str | None: - ... - - # install a repository for the given language and language_version - def install_environment( - self, - prefix: Prefix, - version: str, - additional_dependencies: Sequence[str], - ) -> None: - ... - - # modify the environment for hook execution - def in_env( - self, - prefix: Prefix, - version: str, - ) -> ContextManager[None]: - ... - - # execute a hook and return the exit code and output - def run_hook( - self, - prefix: Prefix, - entry: str, - args: Sequence[str], - file_args: Sequence[str], - *, - is_local: bool, - require_serial: bool, - color: bool, - ) -> tuple[int, bytes]: - ... - - -languages: dict[str, Language] = { - 'conda': conda, - 'coursier': coursier, - 'dart': dart, - 'docker': docker, - 'docker_image': docker_image, - 'dotnet': dotnet, - 'fail': fail, - 'golang': golang, - 'lua': lua, - 'node': node, - 'perl': perl, - 'pygrep': pygrep, - 'python': python, - 'r': r, - 'ruby': ruby, - 'rust': rust, - 'script': script, - 'swift': swift, - 'system': system, - # TODO: fully deprecate `python_venv` - 'python_venv': python, -} -all_languages = sorted(languages) diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index e2fb01969..05f1d2919 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -5,19 +5,19 @@ from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import SubstitutionT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'conda' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def get_env_patch(env: str) -> PatchesT: @@ -41,7 +41,7 @@ def get_env_patch(env: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -60,11 +60,11 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - helpers.assert_version_default('conda', version) + lang_base.assert_version_default('conda', version) conda_exe = _conda_exe() - env_dir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + env_dir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) cmd_output_b( conda_exe, 'env', 'create', '-p', env_dir, '--file', 'environment.yml', cwd=prefix.prefix_dir, diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index 60757588d..9c5fbfe24 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -5,19 +5,19 @@ from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var from pre_commit.errors import FatalError -from pre_commit.languages import helpers from pre_commit.parse_shebang import find_executable from pre_commit.prefix import Prefix ENVIRONMENT_DIR = 'coursier' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def install_environment( @@ -25,7 +25,7 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - helpers.assert_version_default('coursier', version) + lang_base.assert_version_default('coursier', version) # Support both possible executable names (either "cs" or "coursier") cs = find_executable('cs') or find_executable('coursier') @@ -35,12 +35,12 @@ def install_environment( 'executables in the application search path', ) - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) def _install(*opts: str) -> None: assert cs is not None - helpers.run_setup_cmd(prefix, (cs, 'fetch', *opts)) - helpers.run_setup_cmd(prefix, (cs, 'install', '--dir', envdir, *opts)) + lang_base.setup_cmd(prefix, (cs, 'fetch', *opts)) + lang_base.setup_cmd(prefix, (cs, 'install', '--dir', envdir, *opts)) with in_env(prefix, version): channel = prefix.path('.pre-commit-channel') @@ -71,6 +71,6 @@ def get_env_patch(target_dir: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py index e3c1c5855..e8539caa2 100644 --- a/pre_commit/languages/dart.py +++ b/pre_commit/languages/dart.py @@ -7,19 +7,19 @@ from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import win_exe from pre_commit.yaml import yaml_load ENVIRONMENT_DIR = 'dartenv' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def get_env_patch(venv: str) -> PatchesT: @@ -30,7 +30,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -40,9 +40,9 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - helpers.assert_version_default('dart', version) + lang_base.assert_version_default('dart', version) - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) bin_dir = os.path.join(envdir, 'bin') def _install_dir(prefix_p: Prefix, pub_cache: str) -> None: @@ -51,10 +51,10 @@ def _install_dir(prefix_p: Prefix, pub_cache: str) -> None: with open(prefix_p.path('pubspec.yaml')) as f: pubspec_contents = yaml_load(f) - helpers.run_setup_cmd(prefix_p, ('dart', 'pub', 'get'), env=dart_env) + lang_base.setup_cmd(prefix_p, ('dart', 'pub', 'get'), env=dart_env) for executable in pubspec_contents['executables']: - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix_p, ( 'dart', 'compile', 'exe', @@ -77,7 +77,7 @@ def _install_dir(prefix_p: Prefix, pub_cache: str) -> None: else: dep_cmd = (dep,) - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix, ('dart', 'pub', 'cache', 'add', *dep_cmd), env={**os.environ, 'PUB_CACHE': dep_tmp}, diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 2212c5ccb..8e53ca9e3 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -5,16 +5,16 @@ import os from typing import Sequence -from pre_commit.languages import helpers +from pre_commit import lang_base from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'docker' PRE_COMMIT_LABEL = 'PRE_COMMIT' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -in_env = helpers.no_env # no special environment for docker +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +in_env = lang_base.no_env # no special environment for docker def _is_in_docker() -> bool: @@ -84,16 +84,16 @@ def build_docker_image( cmd += ('--pull',) # This must come last for old versions of docker. See #477 cmd += ('.',) - helpers.run_setup_cmd(prefix, cmd) + lang_base.setup_cmd(prefix, cmd) def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: # pragma: win32 no cover - helpers.assert_version_default('docker', version) - helpers.assert_no_additional_deps('docker', additional_dependencies) + lang_base.assert_version_default('docker', version) + lang_base.assert_no_additional_deps('docker', additional_dependencies) - directory = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + directory = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) # Docker doesn't really have relevant disk environment, but pre-commit # still needs to cleanup its state files on failure @@ -135,10 +135,10 @@ def run_hook( # automated cleanup of docker images. build_docker_image(prefix, pull=False) - entry_exe, *cmd_rest = helpers.hook_cmd(entry, args) + entry_exe, *cmd_rest = lang_base.hook_cmd(entry, args) entry_tag = ('--entrypoint', entry_exe, docker_tag(prefix)) - return helpers.run_xargs( + return lang_base.run_xargs( (*docker_cmd(), *entry_tag, *cmd_rest), file_args, require_serial=require_serial, diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index 8e5f2c04c..26f006e4a 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -2,15 +2,15 @@ from typing import Sequence -from pre_commit.languages import helpers +from pre_commit import lang_base from pre_commit.languages.docker import docker_cmd from pre_commit.prefix import Prefix ENVIRONMENT_DIR = None -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -install_environment = helpers.no_install -in_env = helpers.no_env +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +install_environment = lang_base.no_install +in_env = lang_base.no_env def run_hook( @@ -23,8 +23,8 @@ def run_hook( require_serial: bool, color: bool, ) -> tuple[int, bytes]: # pragma: win32 no cover - cmd = docker_cmd() + helpers.hook_cmd(entry, args) - return helpers.run_xargs( + cmd = docker_cmd() + lang_base.hook_cmd(entry, args) + return lang_base.run_xargs( cmd, file_args, require_serial=require_serial, diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index 3db2679d3..e9568f222 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -9,18 +9,18 @@ from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix ENVIRONMENT_DIR = 'dotnetenv' BIN_DIR = 'bin' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def get_env_patch(venv: str) -> PatchesT: @@ -31,7 +31,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -57,14 +57,14 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - helpers.assert_version_default('dotnet', version) - helpers.assert_no_additional_deps('dotnet', additional_dependencies) + lang_base.assert_version_default('dotnet', version) + lang_base.assert_no_additional_deps('dotnet', additional_dependencies) - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) build_dir = prefix.path('pre-commit-build') # Build & pack nupkg file - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix, ( 'dotnet', 'pack', @@ -99,7 +99,7 @@ def install_environment( # Install to bin dir with _nuget_config_no_sources() as nuget_config: - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix, ( 'dotnet', 'tool', 'install', diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index 33df067e4..a8ec6a53d 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -2,14 +2,14 @@ from typing import Sequence -from pre_commit.languages import helpers +from pre_commit import lang_base from pre_commit.prefix import Prefix ENVIRONMENT_DIR = None -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -install_environment = helpers.no_install -in_env = helpers.no_env +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +install_environment = lang_base.no_install +in_env = lang_base.no_env def run_hook( diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 3c4b652fa..bea91e9bd 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -19,17 +19,17 @@ from typing import Sequence import pre_commit.constants as C +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output from pre_commit.util import rmtree ENVIRONMENT_DIR = 'golangenv' -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook _ARCH_ALIASES = { 'x86_64': 'amd64', @@ -60,7 +60,7 @@ def _open_archive(bio: IO[bytes]) -> ContextManager[ExtractAll]: @functools.lru_cache(maxsize=1) def get_default_version() -> str: - if helpers.exe_exists('go'): + if lang_base.exe_exists('go'): return 'system' else: return C.DEFAULT @@ -121,7 +121,7 @@ def _install_go(version: str, dest: str) -> None: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir, version)): yield @@ -131,7 +131,7 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - env_dir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + env_dir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) if version != 'system': _install_go(version, env_dir) @@ -149,9 +149,9 @@ def install_environment( os.path.join(env_dir, '.go', 'bin'), os.environ['PATH'], )) - helpers.run_setup_cmd(prefix, ('go', 'install', './...'), env=env) + lang_base.setup_cmd(prefix, ('go', 'install', './...'), env=env) for dependency in additional_dependencies: - helpers.run_setup_cmd(prefix, ('go', 'install', dependency), env=env) + lang_base.setup_cmd(prefix, ('go', 'install', dependency), env=env) # save some disk space -- we don't need this after installation pkgdir = os.path.join(env_dir, 'pkg') diff --git a/pre_commit/languages/lua.py b/pre_commit/languages/lua.py index ffc40b505..12d066140 100644 --- a/pre_commit/languages/lua.py +++ b/pre_commit/languages/lua.py @@ -6,17 +6,17 @@ from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output ENVIRONMENT_DIR = 'lua_env' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def _get_lua_version() -> str: # pragma: win32 no cover @@ -45,7 +45,7 @@ def get_env_patch(d: str) -> PatchesT: # pragma: win32 no cover @contextlib.contextmanager # pragma: win32 no cover def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -55,9 +55,9 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: # pragma: win32 no cover - helpers.assert_version_default('lua', version) + lang_base.assert_version_default('lua', version) - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with in_env(prefix, version): # luarocks doesn't bootstrap a tree prior to installing # so ensure the directory exists. @@ -66,10 +66,10 @@ def install_environment( # Older luarocks (e.g., 2.4.2) expect the rockspec as an arg for rockspec in prefix.star('.rockspec'): make_cmd = ('luarocks', '--tree', envdir, 'make', rockspec) - helpers.run_setup_cmd(prefix, make_cmd) + lang_base.setup_cmd(prefix, make_cmd) # luarocks can't install multiple packages at once # so install them individually. for dependency in additional_dependencies: cmd = ('luarocks', '--tree', envdir, 'install', dependency) - helpers.run_setup_cmd(prefix, cmd) + lang_base.setup_cmd(prefix, cmd) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 9688da359..66d613637 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -8,11 +8,11 @@ from typing import Sequence import pre_commit.constants as C +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.languages.python import bin_dir from pre_commit.prefix import Prefix from pre_commit.util import cmd_output @@ -20,7 +20,7 @@ from pre_commit.util import rmtree ENVIRONMENT_DIR = 'node_env' -run_hook = helpers.basic_run_hook +run_hook = lang_base.basic_run_hook @functools.lru_cache(maxsize=1) @@ -30,7 +30,7 @@ def get_default_version() -> str: return C.DEFAULT # if node is already installed, we can save a bunch of setup time by # using the installed version - elif all(helpers.exe_exists(exe) for exe in ('node', 'npm')): + elif all(lang_base.exe_exists(exe) for exe in ('node', 'npm')): return 'system' else: return C.DEFAULT @@ -60,13 +60,13 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield -def health_check(prefix: Prefix, language_version: str) -> str | None: - with in_env(prefix, language_version): +def health_check(prefix: Prefix, version: str) -> str | None: + with in_env(prefix, version): retcode, _, _ = cmd_output_b('node', '--version', check=False) if retcode != 0: # pragma: win32 no cover return f'`node --version` returned {retcode}' @@ -78,7 +78,7 @@ def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: assert prefix.exists('package.json') - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx?f=255&MSPPError=-2147217396#maxpath if sys.platform == 'win32': # pragma: no cover @@ -96,13 +96,13 @@ def install_environment( 'npm', 'install', '--dev', '--prod', '--ignore-prepublish', '--no-progress', '--no-save', ) - helpers.run_setup_cmd(prefix, local_install_cmd) + lang_base.setup_cmd(prefix, local_install_cmd) _, pkg, _ = cmd_output('npm', 'pack', cwd=prefix.prefix_dir) pkg = prefix.path(pkg.strip()) install = ('npm', 'install', '-g', pkg, *additional_dependencies) - helpers.run_setup_cmd(prefix, install) + lang_base.setup_cmd(prefix, install) # clean these up after installation if prefix.exists('node_modules'): # pragma: win32 no cover diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py index 2530c0ee1..2a7f16290 100644 --- a/pre_commit/languages/perl.py +++ b/pre_commit/languages/perl.py @@ -6,16 +6,16 @@ from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix ENVIRONMENT_DIR = 'perl_env' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def get_env_patch(venv: str) -> PatchesT: @@ -34,7 +34,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -42,9 +42,9 @@ def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: - helpers.assert_version_default('perl', version) + lang_base.assert_version_default('perl', version) with in_env(prefix, version): - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix, ('cpan', '-T', '.', *additional_dependencies), ) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index f0eb9a959..ec55560b0 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -7,16 +7,16 @@ from typing import Pattern from typing import Sequence +from pre_commit import lang_base from pre_commit import output -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.xargs import xargs ENVIRONMENT_DIR = None -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -install_environment = helpers.no_install -in_env = helpers.no_env +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +install_environment = lang_base.no_install +in_env = lang_base.no_env def _process_filename_by_line(pattern: Pattern[bytes], filename: str) -> int: diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index c373646bc..976674e2b 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -8,11 +8,11 @@ from typing import Sequence import pre_commit.constants as C +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.parse_shebang import find_executable from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError @@ -21,7 +21,7 @@ from pre_commit.util import win_exe ENVIRONMENT_DIR = 'py_env' -run_hook = helpers.basic_run_hook +run_hook = lang_base.basic_run_hook @functools.lru_cache(maxsize=None) @@ -153,13 +153,13 @@ def norm_version(version: str) -> str | None: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield -def health_check(prefix: Prefix, language_version: str) -> str | None: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) +def health_check(prefix: Prefix, version: str) -> str | None: + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) pyvenv_cfg = os.path.join(envdir, 'pyvenv.cfg') # created with "old" virtualenv @@ -202,7 +202,7 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) venv_cmd = [sys.executable, '-mvirtualenv', envdir] python = norm_version(version) if python is not None: @@ -211,4 +211,4 @@ def install_environment( cmd_output_b(*venv_cmd, cwd='/') with in_env(prefix, version): - helpers.run_setup_cmd(prefix, install_cmd) + lang_base.setup_cmd(prefix, install_cmd) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index e2383658a..138a26e1e 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -7,18 +7,18 @@ from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b from pre_commit.util import win_exe ENVIRONMENT_DIR = 'renv' RSCRIPT_OPTS = ('--no-save', '--no-restore', '--no-site-file', '--no-environ') -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check def get_env_patch(venv: str) -> PatchesT: @@ -30,7 +30,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -93,7 +93,7 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - env_dir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + env_dir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) os.makedirs(env_dir, exist_ok=True) shutil.copy(prefix.path('renv.lock'), env_dir) shutil.copytree(prefix.path('renv'), os.path.join(env_dir, 'renv')) @@ -166,7 +166,7 @@ def run_hook( color: bool, ) -> tuple[int, bytes]: cmd = _cmd_from_hook(prefix, entry, args, is_local=is_local) - return helpers.run_xargs( + return lang_base.run_xargs( cmd, file_args, require_serial=require_serial, diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 4416f7280..0ee0a857c 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -9,23 +9,23 @@ from typing import Sequence import pre_commit.constants as C +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError from pre_commit.util import resource_bytesio ENVIRONMENT_DIR = 'rbenv' -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook @functools.lru_cache(maxsize=1) def get_default_version() -> str: - if all(helpers.exe_exists(exe) for exe in ('ruby', 'gem')): + if all(lang_base.exe_exists(exe) for exe in ('ruby', 'gem')): return 'system' else: return C.DEFAULT @@ -68,7 +68,7 @@ def get_env_patch( @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir, version)): yield @@ -83,7 +83,7 @@ def _install_rbenv( prefix: Prefix, version: str, ) -> None: # pragma: win32 no cover - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) _extract_resource('rbenv.tar.gz', prefix.path('.')) shutil.move(prefix.path('rbenv'), envdir) @@ -100,10 +100,10 @@ def _install_ruby( version: str, ) -> None: # pragma: win32 no cover try: - helpers.run_setup_cmd(prefix, ('rbenv', 'download', version)) + lang_base.setup_cmd(prefix, ('rbenv', 'download', version)) except CalledProcessError: # pragma: no cover (usually find with download) # Failed to download from mirror for some reason, build it instead - helpers.run_setup_cmd(prefix, ('rbenv', 'install', version)) + lang_base.setup_cmd(prefix, ('rbenv', 'install', version)) def install_environment( @@ -114,17 +114,17 @@ def install_environment( with in_env(prefix, version): # Need to call this before installing so rbenv's directories # are set up - helpers.run_setup_cmd(prefix, ('rbenv', 'init', '-')) + lang_base.setup_cmd(prefix, ('rbenv', 'init', '-')) if version != C.DEFAULT: _install_ruby(prefix, version) # Need to call this after installing to set up the shims - helpers.run_setup_cmd(prefix, ('rbenv', 'rehash')) + lang_base.setup_cmd(prefix, ('rbenv', 'rehash')) with in_env(prefix, version): - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix, ('gem', 'build', *prefix.star('.gemspec')), ) - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix, ( 'gem', 'install', diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 391fd8657..e98e0d02d 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -11,19 +11,19 @@ from typing import Sequence import pre_commit.constants as C +from pre_commit import lang_base from pre_commit import parse_shebang from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b from pre_commit.util import make_executable from pre_commit.util import win_exe ENVIRONMENT_DIR = 'rustenv' -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook @functools.lru_cache(maxsize=1) @@ -63,7 +63,7 @@ def get_env_patch(target_dir: str, version: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir, version)): yield @@ -78,7 +78,7 @@ def _add_dependencies( crate = f'{name}@{spec or "*"}' crates.append(crate) - helpers.run_setup_cmd(prefix, ('cargo', 'add', *crates)) + lang_base.setup_cmd(prefix, ('cargo', 'add', *crates)) def install_rust_with_toolchain(toolchain: str) -> None: @@ -116,7 +116,7 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) # There are two cases where we might want to specify more dependencies: # as dependencies for the library being built, and as binary packages diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 08325f469..89a3ab2d6 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -2,14 +2,14 @@ from typing import Sequence -from pre_commit.languages import helpers +from pre_commit import lang_base from pre_commit.prefix import Prefix ENVIRONMENT_DIR = None -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -install_environment = helpers.no_install -in_env = helpers.no_env +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +install_environment = lang_base.no_install +in_env = lang_base.no_env def run_hook( @@ -22,9 +22,9 @@ def run_hook( require_serial: bool, color: bool, ) -> tuple[int, bytes]: - cmd = helpers.hook_cmd(entry, args) + cmd = lang_base.hook_cmd(entry, args) cmd = (prefix.path(cmd[0]), *cmd[1:]) - return helpers.run_xargs( + return lang_base.run_xargs( cmd, file_args, require_serial=require_serial, diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index c66ad5fb0..8250ab703 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -5,10 +5,10 @@ from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b @@ -16,9 +16,9 @@ BUILD_CONFIG = 'release' ENVIRONMENT_DIR = 'swift_env' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def get_env_patch(venv: str) -> PatchesT: # pragma: win32 no cover @@ -28,7 +28,7 @@ def get_env_patch(venv: str) -> PatchesT: # pragma: win32 no cover @contextlib.contextmanager # pragma: win32 no cover def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -36,9 +36,9 @@ def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: # pragma: win32 no cover - helpers.assert_version_default('swift', version) - helpers.assert_no_additional_deps('swift', additional_dependencies) - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + lang_base.assert_version_default('swift', version) + lang_base.assert_no_additional_deps('swift', additional_dependencies) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) # Build the swift package os.mkdir(envdir) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 204cad727..f6ad688fa 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -1,10 +1,10 @@ from __future__ import annotations -from pre_commit.languages import helpers +from pre_commit import lang_base ENVIRONMENT_DIR = None -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -install_environment = helpers.no_install -in_env = helpers.no_env -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +install_environment = lang_base.no_install +in_env = lang_base.no_env +run_hook = lang_base.basic_run_hook diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 308e80c70..5183df47a 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -8,13 +8,13 @@ from typing import Sequence import pre_commit.constants as C +from pre_commit.all_languages import languages from pre_commit.clientlib import load_manifest from pre_commit.clientlib import LOCAL from pre_commit.clientlib import META from pre_commit.clientlib import parse_version from pre_commit.hook import Hook -from pre_commit.languages.all import languages -from pre_commit.languages.helpers import environment_dir +from pre_commit.lang_base import environment_dir from pre_commit.prefix import Prefix from pre_commit.store import Store from pre_commit.util import clean_path_on_failure diff --git a/testing/language_helpers.py b/testing/language_helpers.py index 0964fbb44..5ab2af2a9 100644 --- a/testing/language_helpers.py +++ b/testing/language_helpers.py @@ -3,7 +3,7 @@ import os from typing import Sequence -from pre_commit.languages.all import Language +from pre_commit.lang_base import Language from pre_commit.prefix import Prefix diff --git a/tests/languages/all_test.py b/tests/all_languages_test.py similarity index 75% rename from tests/languages/all_test.py rename to tests/all_languages_test.py index 33b8925fb..98c912150 100644 --- a/tests/languages/all_test.py +++ b/tests/all_languages_test.py @@ -1,6 +1,6 @@ from __future__ import annotations -from pre_commit.languages.all import languages +from pre_commit.all_languages import languages def test_python_venv_is_an_alias_to_python(): diff --git a/tests/languages/helpers_test.py b/tests/lang_base_test.py similarity index 78% rename from tests/languages/helpers_test.py rename to tests/lang_base_test.py index c209e7e6d..89a64a1f2 100644 --- a/tests/languages/helpers_test.py +++ b/tests/lang_base_test.py @@ -8,8 +8,8 @@ import pytest import pre_commit.constants as C +from pre_commit import lang_base from pre_commit import parse_shebang -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError @@ -32,42 +32,42 @@ def fake_expanduser(pth): def test_exe_exists_does_not_exist(find_exe_mck, homedir_mck): find_exe_mck.return_value = None - assert helpers.exe_exists('ruby') is False + assert lang_base.exe_exists('ruby') is False def test_exe_exists_exists(find_exe_mck, homedir_mck): find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby') - assert helpers.exe_exists('ruby') is True + assert lang_base.exe_exists('ruby') is True def test_exe_exists_false_if_shim(find_exe_mck, homedir_mck): find_exe_mck.return_value = os.path.normpath('/foo/shims/ruby') - assert helpers.exe_exists('ruby') is False + assert lang_base.exe_exists('ruby') is False def test_exe_exists_false_if_homedir(find_exe_mck, homedir_mck): find_exe_mck.return_value = os.path.normpath('/home/me/somedir/ruby') - assert helpers.exe_exists('ruby') is False + assert lang_base.exe_exists('ruby') is False def test_exe_exists_commonpath_raises_ValueError(find_exe_mck, homedir_mck): find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby') with mock.patch.object(os.path, 'commonpath', side_effect=ValueError): - assert helpers.exe_exists('ruby') is True + assert lang_base.exe_exists('ruby') is True def test_exe_exists_true_when_homedir_is_slash(find_exe_mck): find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby') with mock.patch.object(os.path, 'expanduser', return_value=os.sep): - assert helpers.exe_exists('ruby') is True + assert lang_base.exe_exists('ruby') is True def test_basic_get_default_version(): - assert helpers.basic_get_default_version() == C.DEFAULT + assert lang_base.basic_get_default_version() == C.DEFAULT def test_basic_health_check(): - assert helpers.basic_health_check(Prefix('.'), 'default') is None + assert lang_base.basic_health_check(Prefix('.'), 'default') is None def test_failed_setup_command_does_not_unicode_error(): @@ -79,12 +79,12 @@ def test_failed_setup_command_does_not_unicode_error(): # an assertion that this does not raise `UnicodeError` with pytest.raises(CalledProcessError): - helpers.run_setup_cmd(Prefix('.'), (sys.executable, '-c', script)) + lang_base.setup_cmd(Prefix('.'), (sys.executable, '-c', script)) def test_assert_no_additional_deps(): with pytest.raises(AssertionError) as excinfo: - helpers.assert_no_additional_deps('lang', ['hmmm']) + lang_base.assert_no_additional_deps('lang', ['hmmm']) msg, = excinfo.value.args assert msg == ( 'for now, pre-commit does not support additional_dependencies for ' @@ -96,19 +96,19 @@ def test_assert_no_additional_deps(): def test_target_concurrency_normal(): with mock.patch.object(multiprocessing, 'cpu_count', return_value=123): with mock.patch.dict(os.environ, {}, clear=True): - assert helpers.target_concurrency() == 123 + assert lang_base.target_concurrency() == 123 def test_target_concurrency_testing_env_var(): with mock.patch.dict( os.environ, {'PRE_COMMIT_NO_CONCURRENCY': '1'}, clear=True, ): - assert helpers.target_concurrency() == 1 + assert lang_base.target_concurrency() == 1 def test_target_concurrency_on_travis(): with mock.patch.dict(os.environ, {'TRAVIS': '1'}, clear=True): - assert helpers.target_concurrency() == 2 + assert lang_base.target_concurrency() == 2 def test_target_concurrency_cpu_count_not_implemented(): @@ -116,17 +116,17 @@ def test_target_concurrency_cpu_count_not_implemented(): multiprocessing, 'cpu_count', side_effect=NotImplementedError, ): with mock.patch.dict(os.environ, {}, clear=True): - assert helpers.target_concurrency() == 1 + assert lang_base.target_concurrency() == 1 def test_shuffled_is_deterministic(): seq = [str(i) for i in range(10)] expected = ['4', '0', '5', '1', '8', '6', '2', '3', '7', '9'] - assert helpers._shuffled(seq) == expected + assert lang_base._shuffled(seq) == expected def test_xargs_require_serial_is_not_shuffled(): - ret, out = helpers.run_xargs( + ret, out = lang_base.run_xargs( ('echo',), [str(i) for i in range(10)], require_serial=True, color=False, diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py index f5f9985b8..ec5a87875 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -6,9 +6,9 @@ import re_assert import pre_commit.constants as C +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.languages import golang -from pre_commit.languages import helpers from pre_commit.store import _make_local_repo from testing.language_helpers import run_language @@ -18,7 +18,7 @@ @pytest.fixture def exe_exists_mck(): - with mock.patch.object(helpers, 'exe_exists') as mck: + with mock.patch.object(lang_base, 'exe_exists') as mck: yield mck diff --git a/tests/repository_test.py b/tests/repository_test.py index 332816d25..c04eb3796 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -10,12 +10,12 @@ import re_assert import pre_commit.constants as C +from pre_commit import lang_base +from pre_commit.all_languages import languages from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest from pre_commit.hook import Hook -from pre_commit.languages import helpers from pre_commit.languages import python -from pre_commit.languages.all import languages from pre_commit.prefix import Prefix from pre_commit.repository import _hook_installed from pre_commit.repository import all_hooks @@ -275,7 +275,7 @@ def test_repository_state_compatibility(tempdir_factory, store, v): config = make_config_from_repo(path) hook = _get_hook(config, store, 'foo') - envdir = helpers.environment_dir( + envdir = lang_base.environment_dir( hook.prefix, python.ENVIRONMENT_DIR, hook.language_version, @@ -327,7 +327,7 @@ class MyKeyboardInterrupt(KeyboardInterrupt): # raise as well. with pytest.raises(MyKeyboardInterrupt): with mock.patch.object( - helpers, 'run_setup_cmd', side_effect=MyKeyboardInterrupt, + lang_base, 'setup_cmd', side_effect=MyKeyboardInterrupt, ): with mock.patch.object( shutil, 'rmtree', side_effect=MyKeyboardInterrupt, @@ -336,7 +336,7 @@ class MyKeyboardInterrupt(KeyboardInterrupt): # Should have made an environment, however this environment is broken! hook, = hooks - envdir = helpers.environment_dir( + envdir = lang_base.environment_dir( hook.prefix, python.ENVIRONMENT_DIR, hook.language_version, @@ -359,7 +359,7 @@ def test_invalidated_virtualenv(tempdir_factory, store): hook = _get_hook(config, store, 'foo') # Simulate breaking of the virtualenv - envdir = helpers.environment_dir( + envdir = lang_base.environment_dir( hook.prefix, python.ENVIRONMENT_DIR, hook.language_version, From c3613b954a7155e6143b52cb3f3defcab82ba3ae Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Feb 2023 18:18:08 -0500 Subject: [PATCH 65/91] test things more directly to improve coverage --- tests/languages/python_test.py | 23 +++++++++++++++++++++++ tests/languages/script_test.py | 14 ++++++++++++++ tests/languages/system_test.py | 9 +++++++++ tests/repository_test.py | 16 ---------------- 4 files changed, 46 insertions(+), 16 deletions(-) create mode 100644 tests/languages/script_test.py create mode 100644 tests/languages/system_test.py diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 54fb98feb..8bb284eb9 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -12,6 +12,7 @@ from pre_commit.prefix import Prefix from pre_commit.util import make_executable from pre_commit.util import win_exe +from testing.language_helpers import run_language def test_read_pyvenv_cfg(tmpdir): @@ -210,3 +211,25 @@ def test_unhealthy_then_replaced(python_dir): os.replace(f'{py_exe}.tmp', py_exe) assert python.health_check(prefix, C.DEFAULT) is None + + +def test_language_versioned_python_hook(tmp_path): + setup_py = '''\ +from setuptools import setup +setup( + name='example', + py_modules=['mod'], + entry_points={'console_scripts': ['myexe=mod:main']}, +) +''' + tmp_path.joinpath('setup.py').write_text(setup_py) + tmp_path.joinpath('mod.py').write_text('def main(): print("ohai")') + + # we patch this to force virtualenv executing with `-p` since we can't + # reliably have multiple pythons available in CI + with mock.patch.object( + python, + '_sys_executable_matches', + return_value=False, + ): + assert run_language(tmp_path, python, 'myexe') == (0, b'ohai\n') diff --git a/tests/languages/script_test.py b/tests/languages/script_test.py new file mode 100644 index 000000000..a02f615a9 --- /dev/null +++ b/tests/languages/script_test.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from pre_commit.languages import script +from pre_commit.util import make_executable +from testing.language_helpers import run_language + + +def test_script_language(tmp_path): + exe = tmp_path.joinpath('main') + exe.write_text('#!/usr/bin/env bash\necho hello hello world\n') + make_executable(exe) + + expected = (0, b'hello hello world\n') + assert run_language(tmp_path, script, 'main') == expected diff --git a/tests/languages/system_test.py b/tests/languages/system_test.py new file mode 100644 index 000000000..dcd9cf1e0 --- /dev/null +++ b/tests/languages/system_test.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from pre_commit.languages import system +from testing.language_helpers import run_language + + +def test_system_language(tmp_path): + expected = (0, b'hello hello world\n') + assert run_language(tmp_path, system, 'echo hello hello world') == expected diff --git a/tests/repository_test.py b/tests/repository_test.py index 332816d25..e8b540708 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -143,22 +143,6 @@ def test_python_venv_deprecation(store, caplog): ) -def test_language_versioned_python_hook(tempdir_factory, store): - # we patch this force virtualenv executing with `-p` since we can't - # reliably have multiple pythons available in CI - with mock.patch.object( - python, - '_sys_executable_matches', - return_value=False, - ): - _test_hook_repo( - tempdir_factory, store, 'python3_hooks_repo', - 'python3-hook', - [os.devnull], - f'3\n[{os.devnull!r}]\nHello World\n'.encode(), - ) - - def test_system_hook_with_spaces(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'system_hook_with_spaces_repo', From 0cc2856883adc8910c522f4c8eb4ba2b397ebff0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 Feb 2023 02:06:17 +0000 Subject: [PATCH 66/91] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.0.0 → v1.0.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.0.0...v1.0.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 023f4f683..ad8ffba73 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.0.0 + rev: v1.0.1 hooks: - id: mypy additional_dependencies: [types-all] From 25b8ad752831dbbe9c5469760baffef16f4630f6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Feb 2023 21:32:32 -0500 Subject: [PATCH 67/91] improve unit test coverage of lang_base --- tests/lang_base_test.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/lang_base_test.py b/tests/lang_base_test.py index 89a64a1f2..a532b6a54 100644 --- a/tests/lang_base_test.py +++ b/tests/lang_base_test.py @@ -82,6 +82,21 @@ def test_failed_setup_command_does_not_unicode_error(): lang_base.setup_cmd(Prefix('.'), (sys.executable, '-c', script)) +def test_environment_dir(tmp_path): + ret = lang_base.environment_dir(Prefix(tmp_path), 'langenv', 'default') + assert ret == f'{tmp_path}{os.sep}langenv-default' + + +def test_assert_version_default(): + with pytest.raises(AssertionError) as excinfo: + lang_base.assert_version_default('lang', '1.2.3') + msg, = excinfo.value.args + assert msg == ( + 'for now, pre-commit requires system-installed lang -- ' + 'you selected `language_version: 1.2.3`' + ) + + def test_assert_no_additional_deps(): with pytest.raises(AssertionError) as excinfo: lang_base.assert_no_additional_deps('lang', ['hmmm']) @@ -93,6 +108,14 @@ def test_assert_no_additional_deps(): ) +def test_no_env_noop(tmp_path): + before = os.environ.copy() + with lang_base.no_env(Prefix(tmp_path), '1.2.3'): + inside = os.environ.copy() + after = os.environ.copy() + assert before == inside == after + + def test_target_concurrency_normal(): with mock.patch.object(multiprocessing, 'cpu_count', return_value=123): with mock.patch.dict(os.environ, {}, clear=True): @@ -133,3 +156,18 @@ def test_xargs_require_serial_is_not_shuffled(): ) assert ret == 0 assert out.strip() == b'0 1 2 3 4 5 6 7 8 9' + + +def test_basic_run_hook(tmp_path): + ret, out = lang_base.basic_run_hook( + Prefix(tmp_path), + 'echo hi', + ['hello'], + ['file', 'file', 'file'], + is_local=False, + require_serial=False, + color=False, + ) + assert ret == 0 + out = out.replace(b'\r\n', b'\n') + assert out == b'hi hello file file file\n' From 8d84a7a2702b074a8b46f5e38af28bd576291251 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Feb 2023 21:45:04 -0500 Subject: [PATCH 68/91] resources_bytesio is only used by ruby --- pre_commit/languages/ruby.py | 9 +++++++-- pre_commit/util.py | 5 ----- tests/languages/ruby_test.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 0ee0a857c..76631f253 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -2,10 +2,12 @@ import contextlib import functools +import importlib.resources import os.path import shutil import tarfile from typing import Generator +from typing import IO from typing import Sequence import pre_commit.constants as C @@ -16,13 +18,16 @@ from pre_commit.envcontext import Var from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError -from pre_commit.util import resource_bytesio ENVIRONMENT_DIR = 'rbenv' health_check = lang_base.basic_health_check run_hook = lang_base.basic_run_hook +def _resource_bytesio(filename: str) -> IO[bytes]: + return importlib.resources.open_binary('pre_commit.resources', filename) + + @functools.lru_cache(maxsize=1) def get_default_version() -> str: if all(lang_base.exe_exists(exe) for exe in ('ruby', 'gem')): @@ -74,7 +79,7 @@ def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: def _extract_resource(filename: str, dest: str) -> None: - with resource_bytesio(filename) as bio: + with _resource_bytesio(filename) as bio: with tarfile.open(fileobj=bio) as tf: tf.extractall(dest) diff --git a/pre_commit/util.py b/pre_commit/util.py index 8ea48446a..3d448e318 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -12,7 +12,6 @@ from typing import Any from typing import Callable from typing import Generator -from typing import IO from pre_commit import parse_shebang @@ -36,10 +35,6 @@ def clean_path_on_failure(path: str) -> Generator[None, None, None]: raise -def resource_bytesio(filename: str) -> IO[bytes]: - return importlib.resources.open_binary('pre_commit.resources', filename) - - def resource_text(filename: str) -> str: return importlib.resources.read_text('pre_commit.resources', filename) diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 9cfaad5d0..6397a4347 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -9,8 +9,8 @@ from pre_commit import parse_shebang from pre_commit.envcontext import envcontext from pre_commit.languages import ruby +from pre_commit.languages.ruby import _resource_bytesio from pre_commit.store import _make_local_repo -from pre_commit.util import resource_bytesio from testing.language_helpers import run_language from testing.util import cwd from testing.util import xfailif_windows @@ -40,7 +40,7 @@ def test_uses_system_if_both_gem_and_ruby_are_available(find_exe_mck): ('rbenv.tar.gz', 'ruby-build.tar.gz', 'ruby-download.tar.gz'), ) def test_archive_root_stat(filename): - with resource_bytesio(filename) as f: + with _resource_bytesio(filename) as f: with tarfile.open(fileobj=f) as tarf: root, _, _ = filename.partition('.') assert oct(tarf.getmember(root).mode) == '0o755' From d23990cc8b3b6040c0e5a7455ab7104cd60a5df4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Feb 2023 22:21:31 -0500 Subject: [PATCH 69/91] use run_language for repository_test --- testing/language_helpers.py | 6 ++++-- tests/repository_test.py | 31 +++++++++++++++---------------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/testing/language_helpers.py b/testing/language_helpers.py index 5ab2af2a9..ead8dae27 100644 --- a/testing/language_helpers.py +++ b/testing/language_helpers.py @@ -16,6 +16,8 @@ def run_language( version: str | None = None, deps: Sequence[str] = (), is_local: bool = False, + require_serial: bool = True, + color: bool = False, ) -> tuple[int, bytes]: prefix = Prefix(str(path)) version = version or language.get_default_version() @@ -31,8 +33,8 @@ def run_language( args, file_args, is_local=is_local, - require_serial=True, - color=False, + require_serial=require_serial, + color=color, ) out = out.replace(b'\r\n', b'\n') return ret, out diff --git a/tests/repository_test.py b/tests/repository_test.py index 1af73e3aa..9e5d9d62f 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -25,25 +25,24 @@ from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo from testing.fixtures import modify_manifest +from testing.language_helpers import run_language from testing.util import cwd from testing.util import get_resource_path -def _norm_out(b): - return b.replace(b'\r\n', b'\n') - - def _hook_run(hook, filenames, color): - with languages[hook.language].in_env(hook.prefix, hook.language_version): - return languages[hook.language].run_hook( - hook.prefix, - hook.entry, - hook.args, - filenames, - is_local=hook.src == 'local', - require_serial=hook.require_serial, - color=color, - ) + return run_language( + path=hook.prefix.prefix_dir, + language=languages[hook.language], + exe=hook.entry, + args=hook.args, + file_args=filenames, + version=hook.language_version, + deps=hook.additional_dependencies, + is_local=hook.src == 'local', + require_serial=hook.require_serial, + color=color, + ) def _get_hook_no_install(repo_config, store, hook_id): @@ -77,7 +76,7 @@ def _test_hook_repo( hook = _get_hook(config, store, hook_id) ret, out = _hook_run(hook, args, color=color) assert ret == expected_return_code - assert _norm_out(out) == expected + assert out == expected def test_python_hook(tempdir_factory, store): @@ -425,7 +424,7 @@ def test_local_python_repo(store, local_python_config): assert hook.language_version != C.DEFAULT ret, out = _hook_run(hook, ('filename',), color=False) assert ret == 0 - assert _norm_out(out) == b"['filename']\nHello World\n" + assert out == b"['filename']\nHello World\n" def test_default_language_version(store, local_python_config): From 9655158d938d0f49df0a0eedc5c0d166a45d591a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Feb 2023 17:00:05 -0500 Subject: [PATCH 70/91] test languages only when they are changed --- .github/actions/pre-test/action.yml | 31 ----------- .github/workflows/languages.yaml | 82 +++++++++++++++++++++++++++++ testing/languages | 79 +++++++++++++++++++++++++++ tox.ini | 4 +- 4 files changed, 163 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/languages.yaml create mode 100755 testing/languages diff --git a/.github/actions/pre-test/action.yml b/.github/actions/pre-test/action.yml index 42bbf00b5..9d1eb2de6 100644 --- a/.github/actions/pre-test/action.yml +++ b/.github/actions/pre-test/action.yml @@ -5,36 +5,5 @@ inputs: runs: using: composite steps: - - name: setup (windows) - shell: bash - if: runner.os == 'Windows' - run: | - set -x - - echo 'TEMP=C:\TEMP' >> "$GITHUB_ENV" - - echo "$CONDA\Scripts" >> "$GITHUB_PATH" - - echo 'C:\Strawberry\perl\bin' >> "$GITHUB_PATH" - echo 'C:\Strawberry\perl\site\bin' >> "$GITHUB_PATH" - echo 'C:\Strawberry\c\bin' >> "$GITHUB_PATH" - - testing/get-coursier.sh - testing/get-dart.sh - - name: setup (linux) - shell: bash - if: runner.os == 'Linux' - run: | - set -x - - sudo apt-get update - sudo apt-get install -y --no-install-recommends \ - lua5.3 \ - liblua5.3-dev \ - luarocks - - testing/get-coursier.sh - testing/get-dart.sh - testing/get-swift.sh - uses: asottile/workflows/.github/actions/latest-git@v1.4.0 if: inputs.env == 'py38' && runner.os == 'Linux' diff --git a/.github/workflows/languages.yaml b/.github/workflows/languages.yaml new file mode 100644 index 000000000..8bc8e712f --- /dev/null +++ b/.github/workflows/languages.yaml @@ -0,0 +1,82 @@ +name: languages + +on: + push: + branches: [main, test-me-*] + tags: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + vars: + runs-on: ubuntu-latest + outputs: + languages: ${{ steps.vars.outputs.languages }} + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-python@v4 + with: + python-version: 3.8 + - name: install deps + run: python -mpip install -e . -r requirements-dev.txt + - name: vars + run: testing/languages ${{ github.event_name == 'push' && '--all' || '' }} + id: vars + language: + needs: [vars] + runs-on: ${{ matrix.os }} + if: needs.vars.outputs.languages != '[]' + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON(needs.vars.outputs.languages) }} + steps: + - uses: asottile/workflows/.github/actions/fast-checkout@v1.4.0 + - uses: actions/setup-python@v4 + with: + python-version: 3.8 + + - run: echo "$CONDA\Scripts" >> "$GITHUB_PATH" + shell: bash + if: matrix.os == 'windows-latest' && matrix.language == 'conda' + - run: testing/get-coursier.sh + shell: bash + if: matrix.language == 'coursier' + - run: testing/get-dart.sh + shell: bash + if: matrix.language == 'dart' + - run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + lua5.3 \ + liblua5.3-dev \ + luarocks + if: matrix.os == 'ubuntu-latest' && matrix.language == 'lua' + - run: | + echo 'C:\Strawberry\perl\bin' >> "$GITHUB_PATH" + echo 'C:\Strawberry\perl\site\bin' >> "$GITHUB_PATH" + echo 'C:\Strawberry\c\bin' >> "$GITHUB_PATH" + shell: bash + if: matrix.os == 'windows-latest' && matrix.language == 'perl' + - run: testing/get-swift.sh + if: matrix.os == 'ubuntu-latest' && matrix.language == 'swift' + + - name: install deps + run: python -mpip install -e . -r requirements-dev.txt + - name: run tests + run: coverage run -m pytest tests/languages/${{ matrix.language }}_test.py + - name: check coverage + run: coverage report --include pre_commit/languages/${{ matrix.language }}.py,tests/languages/${{ matrix.language }}_test.py + collector: + needs: [language] + if: always() + runs-on: ubuntu-latest + steps: + - name: check for failures + if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') + run: echo job failed && exit 1 diff --git a/testing/languages b/testing/languages new file mode 100755 index 000000000..5e8fc9e4f --- /dev/null +++ b/testing/languages @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import concurrent.futures +import json +import os.path +import subprocess +import sys + +EXCLUDED = frozenset(( + ('windows-latest', 'docker'), + ('windows-latest', 'docker_image'), + ('windows-latest', 'lua'), + ('windows-latest', 'swift'), +)) + + +def _lang_files(lang: str) -> frozenset[str]: + prog = f'''\ +import json +import os.path +import sys + +import pre_commit.languages.{lang} +import tests.languages.{lang}_test + +modules = sorted( + os.path.relpath(v.__file__) + for k, v in sys.modules.items() + if k.startswith(('pre_commit.', 'tests.', 'testing.')) +) +print(json.dumps(modules)) +''' + out = json.loads(subprocess.check_output((sys.executable, '-c', prog))) + return frozenset(out) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument('--all', action='store_true') + args = parser.parse_args() + + langs = [ + os.path.splitext(fname)[0] + for fname in sorted(os.listdir('pre_commit/languages')) + if fname.endswith('.py') and fname != '__init__.py' + ] + + if not args.all: + with concurrent.futures.ThreadPoolExecutor(os.cpu_count()) as exe: + by_lang = { + lang: files + for lang, files in zip(langs, exe.map(_lang_files, langs)) + } + + diff_cmd = ('git', 'diff', '--name-only', 'origin/main...HEAD') + files = set(subprocess.check_output(diff_cmd).decode().splitlines()) + + langs = [ + lang + for lang, lang_files in by_lang.items() + if lang_files & files + ] + + matched = [ + {'os': os, 'language': lang} + for os in ('windows-latest', 'ubuntu-latest') + for lang in langs + if (os, lang) not in EXCLUDED + ] + + with open(os.environ['GITHUB_OUTPUT'], 'a') as f: + f.write(f'languages={json.dumps(matched)}\n') + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/tox.ini b/tox.ini index a44f93d48..602679a60 100644 --- a/tox.ini +++ b/tox.ini @@ -6,8 +6,8 @@ deps = -rrequirements-dev.txt passenv = * commands = coverage erase - coverage run -m pytest {posargs:tests} - coverage report + coverage run -m pytest {posargs:tests} --ignore=tests/languages + coverage report --omit=pre_commit/languages/*,tests/languages/* [testenv:pre-commit] skip_install = true From 08fa5ffc4353f0d9255281e4914cff2acc1c0859 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 21 Feb 2023 11:06:24 -0500 Subject: [PATCH 71/91] make a change to trigger the language tests --- pre_commit/lang_base.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pre_commit/lang_base.py b/pre_commit/lang_base.py index 6ba412f0e..9480c559f 100644 --- a/pre_commit/lang_base.py +++ b/pre_commit/lang_base.py @@ -43,12 +43,7 @@ def install_environment( ... # modify the environment for hook execution - def in_env( - self, - prefix: Prefix, - version: str, - ) -> ContextManager[None]: - ... + def in_env(self, prefix: Prefix, version: str) -> ContextManager[None]: ... # execute a hook and return the exit code and output def run_hook( From cddc9cff0f05a8d9e3ca126df03962574efe98e9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 21 Feb 2023 12:09:26 -0500 Subject: [PATCH 72/91] only treat exit code 1 as a successful diff --- pre_commit/staged_files_only.py | 23 ++++++++++----- tests/staged_files_only_test.py | 52 +++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 172fb20b1..881235656 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -7,6 +7,7 @@ from typing import Generator from pre_commit import git +from pre_commit.errors import FatalError from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b @@ -49,12 +50,16 @@ def _intent_to_add_cleared() -> Generator[None, None, None]: @contextlib.contextmanager def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: tree = cmd_output('git', 'write-tree')[1].strip() - retcode, diff_stdout_binary, _ = cmd_output_b( + diff_cmd = ( 'git', 'diff-index', '--ignore-submodules', '--binary', '--exit-code', '--no-color', '--no-ext-diff', tree, '--', - check=False, ) - if retcode and diff_stdout_binary.strip(): + retcode, diff_stdout, diff_stderr = cmd_output_b(*diff_cmd, check=False) + if retcode == 0: + # There weren't any staged files so we don't need to do anything + # special + yield + elif retcode == 1 and diff_stdout.strip(): patch_filename = f'patch{int(time.time())}-{os.getpid()}' patch_filename = os.path.join(patch_dir, patch_filename) logger.warning('Unstaged files detected.') @@ -62,7 +67,7 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: # Save the current unstaged changes as a patch os.makedirs(patch_dir, exist_ok=True) with open(patch_filename, 'wb') as patch_file: - patch_file.write(diff_stdout_binary) + patch_file.write(diff_stdout) # prevent recursive post-checkout hooks (#1418) no_checkout_env = dict(os.environ, _PRE_COMMIT_SKIP_POST_CHECKOUT='1') @@ -86,10 +91,12 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: _git_apply(patch_filename) logger.info(f'Restored changes from {patch_filename}.') - else: - # There weren't any staged files so we don't need to do anything - # special - yield + else: # pragma: win32 no cover + # some error occurred while requesting the diff + e = CalledProcessError(retcode, diff_cmd, b'', diff_stderr) + raise FatalError( + f'pre-commit failed to diff -- perhaps due to permissions?\n\n{e}', + ) @contextlib.contextmanager diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index a91f31519..50f146be4 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -1,12 +1,15 @@ from __future__ import annotations +import contextlib import itertools import os.path import shutil import pytest +import re_assert from pre_commit import git +from pre_commit.errors import FatalError from pre_commit.staged_files_only import staged_files_only from pre_commit.util import cmd_output from testing.auto_namedtuple import auto_namedtuple @@ -14,6 +17,7 @@ from testing.util import cwd from testing.util import get_resource_path from testing.util import git_commit +from testing.util import xfailif_windows FOO_CONTENTS = '\n'.join(('1', '2', '3', '4', '5', '6', '7', '8', '')) @@ -382,3 +386,51 @@ def test_intent_to_add(in_git_dir, patch_dir): with staged_files_only(patch_dir): assert_no_diff() assert git.intent_to_add_files() == ['foo'] + + +@contextlib.contextmanager +def _unreadable(f): + orig = os.stat(f).st_mode + os.chmod(f, 0o000) + try: + yield + finally: + os.chmod(f, orig) + + +@xfailif_windows # pragma: win32 no cover +def test_failed_diff_does_not_discard_changes(in_git_dir, patch_dir): + # stage 3 files + for i in range(3): + with open(str(i), 'w') as f: + f.write(str(i)) + cmd_output('git', 'add', '0', '1', '2') + + # modify all of their contents + for i in range(3): + with open(str(i), 'w') as f: + f.write('new contents') + + with _unreadable('1'): + with pytest.raises(FatalError) as excinfo: + with staged_files_only(patch_dir): + raise AssertionError('should have errored on enter') + + # the diff command failed to produce a diff of `1` + msg, = excinfo.value.args + re_assert.Matches( + r'^pre-commit failed to diff -- perhaps due to permissions\?\n\n' + r'command: .*\n' + r'return code: 128\n' + r'stdout: \(none\)\n' + r'stderr:\n' + r' error: open\("1"\): Permission denied\n' + r' fatal: cannot hash 1\n' + # TODO: not sure why there's weird whitespace here + r' $', + ).assert_matches(msg) + + # even though it errored, the unstaged changes should still be present + for i in range(3): + with open(str(i)) as f: + assert f.read() == 'new contents' From 4ded56efac790028557e8ad446937d00dff7f05d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 21 Feb 2023 12:42:09 -0500 Subject: [PATCH 73/91] fix trailing whitespace in CalledProcessError output --- pre_commit/util.py | 2 +- tests/staged_files_only_test.py | 4 +--- tests/util_test.py | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index 3d448e318..ea0d4f525 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -62,7 +62,7 @@ def __init__( def __bytes__(self) -> bytes: def _indent_or_none(part: bytes | None) -> bytes: if part: - return b'\n ' + part.replace(b'\n', b'\n ') + return b'\n ' + part.replace(b'\n', b'\n ').rstrip() else: return b' (none)' diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 50f146be4..58dbe5ac6 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -425,9 +425,7 @@ def test_failed_diff_does_not_discard_changes(in_git_dir, patch_dir): r'stdout: \(none\)\n' r'stderr:\n' r' error: open\("1"\): Permission denied\n' - r' fatal: cannot hash 1\n' - # TODO: not sure why there's weird whitespace here - r' $', + r' fatal: cannot hash 1$', ).assert_matches(msg) # even though it errored, the unstaged changes should still be present diff --git a/tests/util_test.py b/tests/util_test.py index 310f8f58e..5b2621138 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -16,7 +16,7 @@ def test_CalledProcessError_str(): - error = CalledProcessError(1, ('exe',), b'output', b'errors') + error = CalledProcessError(1, ('exe',), b'output\n', b'errors\n') assert str(error) == ( "command: ('exe',)\n" 'return code: 1\n' From a631abdabf0fcc2bb31f85ae33dfdefb958fe03a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 22 Feb 2023 20:31:14 -0500 Subject: [PATCH 74/91] remove sorting for repo key for additional_deps in other languages this order can matter (such as ruby) --- pre_commit/repository.py | 2 +- pre_commit/store.py | 2 +- tests/store_test.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 5183df47a..040f238f0 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -33,7 +33,7 @@ def _state_filename_v2(venv: str) -> str: def _state(additional_deps: Sequence[str]) -> object: - return {'additional_dependencies': sorted(additional_deps)} + return {'additional_dependencies': additional_deps} def _read_state(venv: str) -> object | None: diff --git a/pre_commit/store.py b/pre_commit/store.py index 6ddc7c481..487e3e798 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -125,7 +125,7 @@ def connect( @classmethod def db_repo_name(cls, repo: str, deps: Sequence[str]) -> str: if deps: - return f'{repo}:{",".join(sorted(deps))}' + return f'{repo}:{",".join(deps)}' else: return repo diff --git a/tests/store_test.py b/tests/store_test.py index 146eac416..eaab94000 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -180,7 +180,7 @@ def test_create_when_store_already_exists(store): def test_db_repo_name(store): assert store.db_repo_name('repo', ()) == 'repo' - assert store.db_repo_name('repo', ('b', 'a', 'c')) == 'repo:a,b,c' + assert store.db_repo_name('repo', ('b', 'a', 'c')) == 'repo:b,a,c' def test_local_resources_reflects_reality(): From 294590fd124484a786ba90423fa5d89536a6de98 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 22 Feb 2023 20:53:02 -0500 Subject: [PATCH 75/91] v3.1.0 --- CHANGELOG.md | 18 ++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0998da98b..8a4278120 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +3.1.0 - 2023-02-22 +================== + +### Fixes +- Fix `dotnet` for `.sln`-based hooks for dotnet>=7.0.200. + - #2763 PR by @m-rsha. +- Prevent stashing when `diff` fails to execute. + - #2774 PR by @asottile. + - #2773 issue by @strubbly. +- Dependencies are no longer sorted in repository key. + - #2776 PR by @asottile. + +### Updating +- Deprecate `language: python_venv`. Use `language: python` instead. + - #2746 PR by @asottile. + - #2734 issue by @asottile. + + 3.0.4 - 2023-02-03 ================== diff --git a/setup.cfg b/setup.cfg index 56b856cad..d1f649fe7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.0.4 +version = 3.1.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 2700a7d62241d7bea52d5305b5bca88ad7072919 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 27 Feb 2023 20:49:22 -0500 Subject: [PATCH 76/91] set RUSTUP_HOME when using a non-system rust --- pre_commit/languages/rust.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index e98e0d02d..af5f483d3 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -142,10 +142,15 @@ def install_environment( else: packages_to_install.add((package,)) - with in_env(prefix, version): + with contextlib.ExitStack() as ctx: + ctx.enter_context(in_env(prefix, version)) + if version != 'system': install_rust_with_toolchain(_rust_toolchain(version)) + tmpdir = ctx.enter_context(tempfile.TemporaryDirectory()) + ctx.enter_context(envcontext((('RUSTUP_HOME', tmpdir),))) + if len(lib_deps) > 0: _add_dependencies(prefix, lib_deps) From 2822de9aa6284f2de1c5ff8d0884b38bc553afa5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 27 Feb 2023 21:07:23 -0500 Subject: [PATCH 77/91] v3.1.1 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a4278120..cfcef4530 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +3.1.1 - 2023-02-27 +================== + +### Fixes +- Fix `rust` with `language_version` and a non-writable host `RUSTUP_HOME`. + - pre-commit-ci/issues#173 by @Swiftb0y. + - #2788 by @asottile. + 3.1.0 - 2023-02-22 ================== diff --git a/setup.cfg b/setup.cfg index d1f649fe7..507c0ad13 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.1.0 +version = 3.1.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 5ce4a549d3e0ee441698a13e431cf207bc3b611f Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Fri, 3 Mar 2023 20:16:09 -0600 Subject: [PATCH 78/91] prefer `sys.platform` over `os.name` when checking for windows OS --- pre_commit/languages/conda.py | 3 ++- pre_commit/languages/python.py | 4 ++-- pre_commit/util.py | 2 +- testing/util.py | 3 ++- tests/languages/python_test.py | 4 ++-- tests/parse_shebang_test.py | 2 +- tests/repository_test.py | 3 ++- tests/xargs_test.py | 2 +- 8 files changed, 13 insertions(+), 10 deletions(-) diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index 05f1d2919..41c355e77 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -2,6 +2,7 @@ import contextlib import os +import sys from typing import Generator from typing import Sequence @@ -26,7 +27,7 @@ def get_env_patch(env: str) -> PatchesT: # $CONDA_PREFIX/Scripts and $CONDA_PREFIX. Whereas the latter only # seems to be used for python.exe. path: SubstitutionT = (os.path.join(env, 'bin'), os.pathsep, Var('PATH')) - if os.name == 'nt': # pragma: no cover (platform specific) + if sys.platform == 'win32': # pragma: win32 cover path = (env, os.pathsep, *path) path = (os.path.join(env, 'Scripts'), os.pathsep, *path) path = (os.path.join(env, 'Library', 'bin'), os.pathsep, *path) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 976674e2b..3ef343608 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -48,7 +48,7 @@ def _read_pyvenv_cfg(filename: str) -> dict[str, str]: def bin_dir(venv: str) -> str: """On windows there's a different directory for the virtualenv""" - bin_part = 'Scripts' if os.name == 'nt' else 'bin' + bin_part = 'Scripts' if sys.platform == 'win32' else 'bin' return os.path.join(venv, bin_part) @@ -137,7 +137,7 @@ def norm_version(version: str) -> str | None: elif _sys_executable_matches(version): # virtualenv defaults to our exe return None - if os.name == 'nt': # pragma: no cover (windows) + if sys.platform == 'win32': # pragma: no cover (windows) version_exec = _find_by_py_launcher(version) if version_exec: return version_exec diff --git a/pre_commit/util.py b/pre_commit/util.py index ea0d4f525..4f8e8357d 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -119,7 +119,7 @@ def cmd_output(*cmd: str, **kwargs: Any) -> tuple[int, str, str | None]: return returncode, stdout, stderr -if os.name != 'nt': # pragma: win32 no cover +if sys.platform != 'win32': # pragma: win32 no cover from os import openpty import termios diff --git a/testing/util.py b/testing/util.py index 7c68d0eee..0fee28265 100644 --- a/testing/util.py +++ b/testing/util.py @@ -3,6 +3,7 @@ import contextlib import os.path import subprocess +import sys import pytest @@ -30,7 +31,7 @@ def cmd_output_mocked_pre_commit_home( return ret, out.replace('\r\n', '\n'), None -xfailif_windows = pytest.mark.xfail(os.name == 'nt', reason='windows') +xfailif_windows = pytest.mark.xfail(sys.platform == 'win32', reason='windows') def run_opts( diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 8bb284eb9..a4000b416 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -36,10 +36,10 @@ def test_read_pyvenv_cfg_non_utf8(tmpdir): def test_norm_version_expanduser(): home = os.path.expanduser('~') - if os.name == 'nt': # pragma: nt cover + if sys.platform == 'win32': # pragma: win32 cover path = r'~\python343' expected_path = fr'{home}\python343' - else: # pragma: nt no cover + else: # pragma: win32 no cover path = '~/.pyenv/versions/3.4.3/bin/python' expected_path = f'{home}/.pyenv/versions/3.4.3/bin/python' result = python.norm_version(path) diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index 2fcb29ee7..dd97ca5d8 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -94,7 +94,7 @@ def test_normexe_does_not_exist_sep(): assert excinfo.value.args == ('Executable `./i-dont-exist-lol` not found',) -@pytest.mark.xfail(os.name == 'nt', reason='posix only') +@pytest.mark.xfail(sys.platform == 'win32', reason='posix only') def test_normexe_not_executable(tmpdir): # pragma: win32 no cover tmpdir.join('exe').ensure() with tmpdir.as_cwd(), pytest.raises(OSError) as excinfo: diff --git a/tests/repository_test.py b/tests/repository_test.py index 9e5d9d62f..8fe6e02bb 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -2,6 +2,7 @@ import os.path import shutil +import sys from typing import Any from unittest import mock @@ -198,7 +199,7 @@ def test_intermixed_stdout_stderr(tempdir_factory, store): ) -@pytest.mark.xfail(os.name == 'nt', reason='ptys are posix-only') +@pytest.mark.xfail(sys.platform == 'win32', reason='ptys are posix-only') def test_output_isatty(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'stdout_stderr_repo', diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 0530e50d1..7c41f98cd 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -187,7 +187,7 @@ def test_xargs_propagate_kwargs_to_cmd(): assert b'Pre commit is awesome' in stdout -@pytest.mark.xfail(os.name == 'nt', reason='posix only') +@pytest.mark.xfail(sys.platform == 'win32', reason='posix only') def test_xargs_color_true_makes_tty(): retcode, out = xargs.xargs( (sys.executable, '-c', 'import sys; print(sys.stdout.isatty())'), From 0616c0abf75d45d2bd793ced4b3bddc42b478662 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 Mar 2023 02:52:32 +0000 Subject: [PATCH 79/91] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-autopep8: v2.0.1 → v2.0.2](https://github.com/pre-commit/mirrors-autopep8/compare/v2.0.1...v2.0.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ad8ffba73..0aa2e9ea2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v2.0.1 + rev: v2.0.2 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 From 63a180a935dc0096d23a65aa48b84498b57b8760 Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Sun, 5 Mar 2023 05:16:00 -0600 Subject: [PATCH 80/91] rewrite `args with spaces` test to not require python --- tests/repository_test.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/tests/repository_test.py b/tests/repository_test.py index 8fe6e02bb..a6c58bc7d 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1,6 +1,7 @@ from __future__ import annotations import os.path +import shlex import shutil import sys from typing import Any @@ -17,6 +18,7 @@ from pre_commit.clientlib import load_manifest from pre_commit.hook import Hook from pre_commit.languages import python +from pre_commit.languages import system from pre_commit.prefix import Prefix from pre_commit.repository import _hook_installed from pre_commit.repository import all_hooks @@ -99,22 +101,6 @@ def test_python_hook_default_version(tempdir_factory, store): test_python_hook(tempdir_factory, store) -def test_python_hook_args_with_spaces(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'python_hooks_repo', - 'foo', - [], - b"['i have spaces', 'and\"\\'quotes', '$and !this']\n" - b'Hello World\n', - config_kwargs={ - 'hooks': [{ - 'id': 'foo', - 'args': ['i have spaces', 'and"\'quotes', '$and !this'], - }], - }, - ) - - def test_python_hook_weird_setup_cfg(in_git_dir, tempdir_factory, store): in_git_dir.join('setup.cfg').write('[install]\ninstall_scripts=/usr/sbin') @@ -583,3 +569,14 @@ def test_non_installable_hook_error_for_additional_dependencies(store, caplog): 'using language `system` which does not install an environment. ' 'Perhaps you meant to use a specific language?' ) + + +def test_args_with_spaces_and_quotes(tmp_path): + ret = run_language( + tmp_path, system, + f"{shlex.quote(sys.executable)} -c 'import sys; print(sys.argv[1:])'", + ('i have spaces', 'and"\'quotes', '$and !this'), + ) + + expected = b"['i have spaces', 'and\"\\'quotes', '$and !this']\n" + assert ret == (0, expected) From 8ab9747b339df5bfbf0b7ebb7ebd1885ad6baabd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 9 Mar 2023 11:00:31 -0500 Subject: [PATCH 81/91] show 20 slowest durations in CI --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 602679a60..609c2fe18 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,7 @@ deps = -rrequirements-dev.txt passenv = * commands = coverage erase - coverage run -m pytest {posargs:tests} --ignore=tests/languages + coverage run -m pytest {posargs:tests} --ignore=tests/languages --durations=20 coverage report --omit=pre_commit/languages/*,tests/languages/* [testenv:pre-commit] From e3e17a1617b90c081e043db32cb046ed010f2310 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 11 Mar 2023 14:15:49 -0500 Subject: [PATCH 82/91] make --hook-type and stages match --- pre_commit/clientlib.py | 67 ++++++++++++++++++++++++++++---- pre_commit/commands/hook_impl.py | 2 +- pre_commit/constants.py | 13 ------- pre_commit/main.py | 8 +++- testing/util.py | 2 +- tests/clientlib_test.py | 48 +++++++++++++++++++++++ tests/commands/hook_impl_test.py | 4 +- tests/commands/run_test.py | 14 +++---- tests/main_test.py | 6 +++ tests/repository_test.py | 25 +++++++----- 10 files changed, 147 insertions(+), 42 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 9ff38c6a2..cb7778bb2 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -6,6 +6,7 @@ import shlex import sys from typing import Any +from typing import NamedTuple from typing import Sequence import cfgv @@ -20,6 +21,20 @@ check_string_regex = cfgv.check_and(cfgv.check_string, cfgv.check_regex) +HOOK_TYPES = ( + 'commit-msg', + 'post-checkout', + 'post-commit', + 'post-merge', + 'post-rewrite', + 'pre-commit', + 'pre-merge-commit', + 'pre-push', + 'prepare-commit-msg', +) +# `manual` is not invoked by any installed git hook. See #719 +STAGES = (*HOOK_TYPES, 'manual') + def check_type_tag(tag: str) -> None: if tag not in ALL_TAGS: @@ -43,6 +58,46 @@ def check_min_version(version: str) -> None: ) +_STAGES = { + 'commit': 'pre-commit', + 'merge-commit': 'pre-merge-commit', + 'push': 'pre-push', +} + + +def transform_stage(stage: str) -> str: + return _STAGES.get(stage, stage) + + +class StagesMigrationNoDefault(NamedTuple): + key: str + default: Sequence[str] + + def check(self, dct: dict[str, Any]) -> None: + if self.key not in dct: + return + + val = dct[self.key] + cfgv.check_array(cfgv.check_any)(val) + + val = [transform_stage(v) for v in val] + cfgv.check_array(cfgv.check_one_of(STAGES))(val) + + def apply_default(self, dct: dict[str, Any]) -> None: + if self.key not in dct: + return + dct[self.key] = [transform_stage(v) for v in dct[self.key]] + + def remove_default(self, dct: dict[str, Any]) -> None: + raise NotImplementedError + + +class StagesMigration(StagesMigrationNoDefault): + def apply_default(self, dct: dict[str, Any]) -> None: + dct.setdefault(self.key, self.default) + super().apply_default(dct) + + MANIFEST_HOOK_DICT = cfgv.Map( 'Hook', 'id', @@ -70,7 +125,7 @@ def check_min_version(version: str) -> None: cfgv.Optional('log_file', cfgv.check_string, ''), cfgv.Optional('minimum_pre_commit_version', cfgv.check_string, '0'), cfgv.Optional('require_serial', cfgv.check_bool, False), - cfgv.Optional('stages', cfgv.check_array(cfgv.check_one_of(C.STAGES)), []), + StagesMigration('stages', []), cfgv.Optional('verbose', cfgv.check_bool, False), ) MANIFEST_SCHEMA = cfgv.Array(MANIFEST_HOOK_DICT) @@ -241,7 +296,9 @@ def check(self, dct: dict[str, Any]) -> None: cfgv.OptionalNoDefault(item.key, item.check_fn) for item in MANIFEST_HOOK_DICT.items if item.key != 'id' + if item.key != 'stages' ), + StagesMigrationNoDefault('stages', []), OptionalSensibleRegexAtHook('files', cfgv.check_string), OptionalSensibleRegexAtHook('exclude', cfgv.check_string), ) @@ -290,17 +347,13 @@ def check(self, dct: dict[str, Any]) -> None: cfgv.RequiredRecurse('repos', cfgv.Array(CONFIG_REPO_DICT)), cfgv.Optional( 'default_install_hook_types', - cfgv.check_array(cfgv.check_one_of(C.HOOK_TYPES)), + cfgv.check_array(cfgv.check_one_of(HOOK_TYPES)), ['pre-commit'], ), cfgv.OptionalRecurse( 'default_language_version', DEFAULT_LANGUAGE_VERSION, {}, ), - cfgv.Optional( - 'default_stages', - cfgv.check_array(cfgv.check_one_of(C.STAGES)), - C.STAGES, - ), + StagesMigration('default_stages', STAGES), cfgv.Optional('files', check_string_regex, ''), cfgv.Optional('exclude', check_string_regex, '^$'), cfgv.Optional('fail_fast', cfgv.check_bool, False), diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index f5995e9ad..25d99c297 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -84,7 +84,7 @@ def _ns( ) -> argparse.Namespace: return argparse.Namespace( color=color, - hook_stage=hook_type.replace('pre-', ''), + hook_stage=hook_type, remote_branch=remote_branch, local_branch=local_branch, from_ref=from_ref, diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 3f03ceed9..79a9bb692 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -10,17 +10,4 @@ VERSION = importlib.metadata.version('pre_commit') -# `manual` is not invoked by any installed git hook. See #719 -STAGES = ( - 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', - 'post-commit', 'manual', 'post-checkout', 'push', 'post-merge', - 'post-rewrite', -) - -HOOK_TYPES = ( - 'pre-commit', 'pre-merge-commit', 'pre-push', 'prepare-commit-msg', - 'commit-msg', 'post-commit', 'post-checkout', 'post-merge', - 'post-rewrite', -) - DEFAULT = 'default' diff --git a/pre_commit/main.py b/pre_commit/main.py index 3915993ff..62d171e66 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -7,6 +7,7 @@ from typing import Sequence import pre_commit.constants as C +from pre_commit import clientlib from pre_commit import git from pre_commit.color import add_color_option from pre_commit.commands.autoupdate import autoupdate @@ -52,7 +53,7 @@ def _add_config_option(parser: argparse.ArgumentParser) -> None: def _add_hook_type_option(parser: argparse.ArgumentParser) -> None: parser.add_argument( '-t', '--hook-type', - choices=C.HOOK_TYPES, action='append', dest='hook_types', + choices=clientlib.HOOK_TYPES, action='append', dest='hook_types', ) @@ -73,7 +74,10 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: help='When hooks fail, run `git diff` directly afterward.', ) parser.add_argument( - '--hook-stage', choices=C.STAGES, default='commit', + '--hook-stage', + choices=clientlib.STAGES, + type=clientlib.transform_stage, + default='pre-commit', help='The stage during which the hook is fired. One of %(choices)s', ) parser.add_argument( diff --git a/testing/util.py b/testing/util.py index 0fee28265..8e3934cf2 100644 --- a/testing/util.py +++ b/testing/util.py @@ -46,7 +46,7 @@ def run_opts( to_ref='', remote_name='', remote_url='', - hook_stage='commit', + hook_stage='pre-commit', show_diff_on_failure=False, commit_msg_filename='', prepare_commit_message_source='', diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index efb2aa84a..568b2e974 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -12,6 +12,7 @@ from pre_commit.clientlib import CONFIG_REPO_DICT from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import DEFAULT_LANGUAGE_VERSION +from pre_commit.clientlib import MANIFEST_HOOK_DICT from pre_commit.clientlib import MANIFEST_SCHEMA from pre_commit.clientlib import META_HOOK_DICT from pre_commit.clientlib import OptionalSensibleRegexAtHook @@ -416,3 +417,50 @@ def test_warn_additional(schema): x for x in schema.items if isinstance(x, cfgv.WarnAdditionalKeys) ) assert allowed_keys == set(warn_additional.keys) + + +def test_stages_migration_for_default_stages(): + cfg = { + 'default_stages': ['commit-msg', 'push', 'commit', 'merge-commit'], + 'repos': [], + } + cfgv.validate(cfg, CONFIG_SCHEMA) + cfg = cfgv.apply_defaults(cfg, CONFIG_SCHEMA) + assert cfg['default_stages'] == [ + 'commit-msg', 'pre-push', 'pre-commit', 'pre-merge-commit', + ] + + +def test_manifest_stages_defaulting(): + dct = { + 'id': 'fake-hook', + 'name': 'fake-hook', + 'entry': 'fake-hook', + 'language': 'system', + 'stages': ['commit-msg', 'push', 'commit', 'merge-commit'], + } + cfgv.validate(dct, MANIFEST_HOOK_DICT) + dct = cfgv.apply_defaults(dct, MANIFEST_HOOK_DICT) + assert dct['stages'] == [ + 'commit-msg', 'pre-push', 'pre-commit', 'pre-merge-commit', + ] + + +def test_config_hook_stages_defaulting_missing(): + dct = {'id': 'fake-hook'} + cfgv.validate(dct, CONFIG_HOOK_DICT) + dct = cfgv.apply_defaults(dct, CONFIG_HOOK_DICT) + assert dct == {'id': 'fake-hook'} + + +def test_config_hook_stages_defaulting(): + dct = { + 'id': 'fake-hook', + 'stages': ['commit-msg', 'push', 'commit', 'merge-commit'], + } + cfgv.validate(dct, CONFIG_HOOK_DICT) + dct = cfgv.apply_defaults(dct, CONFIG_HOOK_DICT) + assert dct == { + 'id': 'fake-hook', + 'stages': ['commit-msg', 'pre-push', 'pre-commit', 'pre-merge-commit'], + } diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index aa321dabc..169e1414b 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -142,7 +142,7 @@ def test_check_args_length_prepare_commit_msg_error(): def test_run_ns_pre_commit(): ns = hook_impl._run_ns('pre-commit', True, (), b'') assert ns is not None - assert ns.hook_stage == 'commit' + assert ns.hook_stage == 'pre-commit' assert ns.color is True @@ -245,7 +245,7 @@ def test_run_ns_pre_push_updating_branch(push_example): ns = hook_impl._run_ns('pre-push', False, args, stdin) assert ns is not None - assert ns.hook_stage == 'push' + assert ns.hook_stage == 'pre-push' assert ns.color is False assert ns.remote_name == 'origin' assert ns.remote_url == src diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index f1085d9bb..885b78d61 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -354,13 +354,13 @@ def test_show_diff_on_failure( ({'hook': 'bash_hook'}, (b'Bash hook', b'Passed'), 0, True), ( {'hook': 'nope'}, - (b'No hook with id `nope` in stage `commit`',), + (b'No hook with id `nope` in stage `pre-commit`',), 1, True, ), ( - {'hook': 'nope', 'hook_stage': 'push'}, - (b'No hook with id `nope` in stage `push`',), + {'hook': 'nope', 'hook_stage': 'pre-push'}, + (b'No hook with id `nope` in stage `pre-push`',), 1, True, ), @@ -818,7 +818,7 @@ def test_stages(cap_out, store, repo_with_passing_hook): 'language': 'pygrep', 'stages': [stage], } - for i, stage in enumerate(('commit', 'push', 'manual'), 1) + for i, stage in enumerate(('pre-commit', 'pre-push', 'manual'), 1) ], } add_config_to_repo(repo_with_passing_hook, config) @@ -833,8 +833,8 @@ def _run_for_stage(stage): assert printed.count(b'hook ') == 1 return printed - assert _run_for_stage('commit').startswith(b'hook 1...') - assert _run_for_stage('push').startswith(b'hook 2...') + assert _run_for_stage('pre-commit').startswith(b'hook 1...') + assert _run_for_stage('pre-push').startswith(b'hook 2...') assert _run_for_stage('manual').startswith(b'hook 3...') @@ -1173,7 +1173,7 @@ def test_args_hook_only(cap_out, store, repo_with_passing_hook): ), 'language': 'system', 'files': r'\.py$', - 'stages': ['commit'], + 'stages': ['pre-commit'], }, { 'id': 'do_not_commit', diff --git a/tests/main_test.py b/tests/main_test.py index 511592622..945349fa4 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -216,3 +216,9 @@ def test_expected_fatal_error_no_git_repo(in_tmpdir, cap_out, mock_store_dir): 'Is it installed, and are you in a Git repository directory?' ) assert cap_out_lines[-1] == f'Check the log at {log_file}' + + +def test_hook_stage_migration(mock_store_dir): + with mock.patch.object(main, 'run') as mck: + main.main(('run', '--hook-stage', 'commit')) + assert mck.call_args[0][2].hook_stage == 'pre-commit' diff --git a/tests/repository_test.py b/tests/repository_test.py index a6c58bc7d..903574ce3 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -417,7 +417,7 @@ def test_local_python_repo(store, local_python_config): def test_default_language_version(store, local_python_config): config: dict[str, Any] = { 'default_language_version': {'python': 'fake'}, - 'default_stages': ['commit'], + 'default_stages': ['pre-commit'], 'repos': [local_python_config], } @@ -434,18 +434,18 @@ def test_default_language_version(store, local_python_config): def test_default_stages(store, local_python_config): config: dict[str, Any] = { 'default_language_version': {'python': C.DEFAULT}, - 'default_stages': ['commit'], + 'default_stages': ['pre-commit'], 'repos': [local_python_config], } # `stages` was not set, should default hook, = all_hooks(config, store) - assert hook.stages == ['commit'] + assert hook.stages == ['pre-commit'] # `stages` is set, should not default - config['repos'][0]['hooks'][0]['stages'] = ['push'] + config['repos'][0]['hooks'][0]['stages'] = ['pre-push'] hook, = all_hooks(config, store) - assert hook.stages == ['push'] + assert hook.stages == ['pre-push'] def test_hook_id_not_present(tempdir_factory, store, caplog): @@ -513,11 +513,18 @@ def test_manifest_hooks(tempdir_factory, store): name='Bash hook', pass_filenames=True, require_serial=False, - stages=( - 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', - 'post-commit', 'manual', 'post-checkout', 'push', 'post-merge', + stages=[ + 'commit-msg', + 'post-checkout', + 'post-commit', + 'post-merge', 'post-rewrite', - ), + 'pre-commit', + 'pre-merge-commit', + 'pre-push', + 'prepare-commit-msg', + 'manual', + ], types=['file'], types_or=[], verbose=False, From f39154f69f864457595b21f00e81f0e989d05ddf Mon Sep 17 00:00:00 2001 From: Marcelo Galigniana Date: Fri, 27 Jan 2023 16:18:06 -0300 Subject: [PATCH 83/91] Add pre-rebase hook support --- pre_commit/clientlib.py | 1 + pre_commit/commands/hook_impl.py | 17 ++++++++++ pre_commit/commands/run.py | 5 +++ pre_commit/main.py | 11 +++++++ testing/util.py | 4 +++ tests/commands/hook_impl_test.py | 25 +++++++++++++++ tests/commands/install_uninstall_test.py | 40 ++++++++++++++++++++++++ tests/commands/run_test.py | 10 ++++++ tests/repository_test.py | 1 + 9 files changed, 114 insertions(+) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index cb7778bb2..d0651cae2 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -30,6 +30,7 @@ 'pre-commit', 'pre-merge-commit', 'pre-push', + 'pre-rebase', 'prepare-commit-msg', ) # `manual` is not invoked by any installed git hook. See #719 diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index 25d99c297..dab2135d4 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -73,6 +73,8 @@ def _ns( local_branch: str | None = None, from_ref: str | None = None, to_ref: str | None = None, + pre_rebase_upstream: str | None = None, + pre_rebase_branch: str | None = None, remote_name: str | None = None, remote_url: str | None = None, commit_msg_filename: str | None = None, @@ -89,6 +91,8 @@ def _ns( local_branch=local_branch, from_ref=from_ref, to_ref=to_ref, + pre_rebase_upstream=pre_rebase_upstream, + pre_rebase_branch=pre_rebase_branch, remote_name=remote_name, remote_url=remote_url, commit_msg_filename=commit_msg_filename, @@ -185,6 +189,12 @@ def _check_args_length(hook_type: str, args: Sequence[str]) -> None: f'hook-impl for {hook_type} expected 1, 2, or 3 arguments ' f'but got {len(args)}: {args}', ) + elif hook_type == 'pre-rebase': + if len(args) < 1 or len(args) > 2: + raise SystemExit( + f'hook-impl for {hook_type} expected 1 or 2 arguments ' + f'but got {len(args)}: {args}', + ) elif hook_type in _EXPECTED_ARG_LENGTH_BY_HOOK: expected = _EXPECTED_ARG_LENGTH_BY_HOOK[hook_type] if len(args) != expected: @@ -231,6 +241,13 @@ def _run_ns( return _ns(hook_type, color, is_squash_merge=args[0]) elif hook_type == 'post-rewrite': return _ns(hook_type, color, rewrite_command=args[0]) + elif hook_type == 'pre-rebase' and len(args) == 1: + return _ns(hook_type, color, pre_rebase_upstream=args[0]) + elif hook_type == 'pre-rebase' and len(args) == 2: + return _ns( + hook_type, color, pre_rebase_upstream=args[0], + pre_rebase_branch=args[1], + ) else: raise AssertionError(f'unexpected hook type: {hook_type}') diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index c9bc55b42..c867799e8 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -254,6 +254,7 @@ def _all_filenames(args: argparse.Namespace) -> Collection[str]: # these hooks do not operate on files if args.hook_stage in { 'post-checkout', 'post-commit', 'post-merge', 'post-rewrite', + 'pre-rebase', }: return () elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}: @@ -389,6 +390,10 @@ def run( environ['PRE_COMMIT_FROM_REF'] = args.from_ref environ['PRE_COMMIT_TO_REF'] = args.to_ref + if args.pre_rebase_upstream and args.pre_rebase_branch: + environ['PRE_COMMIT_PRE_REBASE_UPSTREAM'] = args.pre_rebase_upstream + environ['PRE_COMMIT_PRE_REBASE_BRANCH'] = args.pre_rebase_branch + if ( args.remote_name and args.remote_url and args.remote_branch and args.local_branch diff --git a/pre_commit/main.py b/pre_commit/main.py index 62d171e66..9615c5e14 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -107,6 +107,17 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: 'now checked out.' ), ) + parser.add_argument( + '--pre-rebase-upstream', help=( + 'The upstream from which the series was forked.' + ), + ) + parser.add_argument( + '--pre-rebase-branch', help=( + 'The branch being rebased, and is not set when ' + 'rebasing the current branch.' + ), + ) parser.add_argument( '--commit-msg-filename', help='Filename to check when running during `commit-msg`', diff --git a/testing/util.py b/testing/util.py index 8e3934cf2..08d52cbc3 100644 --- a/testing/util.py +++ b/testing/util.py @@ -44,6 +44,8 @@ def run_opts( local_branch='', from_ref='', to_ref='', + pre_rebase_upstream='', + pre_rebase_branch='', remote_name='', remote_url='', hook_stage='pre-commit', @@ -67,6 +69,8 @@ def run_opts( local_branch=local_branch, from_ref=from_ref, to_ref=to_ref, + pre_rebase_upstream=pre_rebase_upstream, + pre_rebase_branch=pre_rebase_branch, remote_name=remote_name, remote_url=remote_url, hook_stage=hook_stage, diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index 169e1414b..d757e85c0 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -100,6 +100,8 @@ def call(*_, **__): ('commit-msg', ['.git/COMMIT_EDITMSG']), ('post-commit', []), ('post-merge', ['1']), + ('pre-rebase', ['main', 'topic']), + ('pre-rebase', ['main']), ('post-checkout', ['old_head', 'new_head', '1']), ('post-rewrite', ['amend']), # multiple choices for commit-editmsg @@ -139,6 +141,13 @@ def test_check_args_length_prepare_commit_msg_error(): ) +def test_check_args_length_pre_rebase_error(): + with pytest.raises(SystemExit) as excinfo: + hook_impl._check_args_length('pre-rebase', []) + msg, = excinfo.value.args + assert msg == 'hook-impl for pre-rebase expected 1 or 2 arguments but got 0: []' # noqa: E501 + + def test_run_ns_pre_commit(): ns = hook_impl._run_ns('pre-commit', True, (), b'') assert ns is not None @@ -146,6 +155,22 @@ def test_run_ns_pre_commit(): assert ns.color is True +def test_run_ns_pre_rebase(): + ns = hook_impl._run_ns('pre-rebase', True, ('main', 'topic'), b'') + assert ns is not None + assert ns.hook_stage == 'pre-rebase' + assert ns.color is True + assert ns.pre_rebase_upstream == 'main' + assert ns.pre_rebase_branch == 'topic' + + ns = hook_impl._run_ns('pre-rebase', True, ('main',), b'') + assert ns is not None + assert ns.hook_stage == 'pre-rebase' + assert ns.color is True + assert ns.pre_rebase_upstream == 'main' + assert ns.pre_rebase_branch is None + + def test_run_ns_commit_msg(): ns = hook_impl._run_ns('commit-msg', False, ('.git/COMMIT_MSG',), b'') assert ns is not None diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index a1ecda867..8b0d3ece4 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -810,6 +810,46 @@ def test_post_merge_integration(tempdir_factory, store): assert os.path.exists('post-merge.tmp') +def test_pre_rebase_integration(tempdir_factory, store): + path = git_dir(tempdir_factory) + config = { + 'repos': [ + { + 'repo': 'local', + 'hooks': [{ + 'id': 'pre-rebase', + 'name': 'Pre rebase', + 'entry': 'touch pre-rebase.tmp', + 'language': 'system', + 'always_run': True, + 'verbose': True, + 'stages': ['pre-rebase'], + }], + }, + ], + } + write_config(path, config) + with cwd(path): + install(C.CONFIG_FILE, store, hook_types=['pre-rebase']) + open('foo', 'a').close() + cmd_output('git', 'add', '.') + git_commit() + + cmd_output('git', 'checkout', '-b', 'branch') + open('bar', 'a').close() + cmd_output('git', 'add', '.') + git_commit() + + cmd_output('git', 'checkout', 'master') + open('baz', 'a').close() + cmd_output('git', 'add', '.') + git_commit() + + cmd_output('git', 'checkout', 'branch') + cmd_output('git', 'rebase', 'master', 'branch') + assert os.path.exists('pre-rebase.tmp') + + def test_post_rewrite_integration(tempdir_factory, store): path = git_dir(tempdir_factory) config = { diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 885b78d61..dd15b94c5 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -563,6 +563,16 @@ def test_merge_conflict_resolved(cap_out, store, in_merge_conflict): assert msg in printed +def test_rebase(cap_out, store, repo_with_passing_hook): + args = run_opts(pre_rebase_upstream='master', pre_rebase_branch='topic') + environ: MutableMapping[str, str] = {} + ret, printed = _do_run( + cap_out, store, repo_with_passing_hook, args, environ, + ) + assert environ['PRE_COMMIT_PRE_REBASE_UPSTREAM'] == 'master' + assert environ['PRE_COMMIT_PRE_REBASE_BRANCH'] == 'topic' + + @pytest.mark.parametrize( ('hooks', 'expected'), ( diff --git a/tests/repository_test.py b/tests/repository_test.py index 903574ce3..045656689 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -522,6 +522,7 @@ def test_manifest_hooks(tempdir_factory, store): 'pre-commit', 'pre-merge-commit', 'pre-push', + 'pre-rebase', 'prepare-commit-msg', 'manual', ], From d3c0a66d23b5cebc060f48278ddb43bcc3384dfc Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Sun, 12 Mar 2023 08:24:38 -0500 Subject: [PATCH 84/91] move slowest python-specific tests out of repository_test --- tests/languages/python_test.py | 51 ++++++++++++++++++++++++++++++++++ tests/repository_test.py | 29 ------------------- 2 files changed, 51 insertions(+), 29 deletions(-) diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index a4000b416..ab26e14e7 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -233,3 +233,54 @@ def test_language_versioned_python_hook(tmp_path): return_value=False, ): assert run_language(tmp_path, python, 'myexe') == (0, b'ohai\n') + + +def _make_hello_hello(tmp_path): + setup_py = '''\ +from setuptools import setup + +setup( + name='socks', + version='0.0.0', + py_modules=['socks'], + entry_points={'console_scripts': ['socks = socks:main']}, +) +''' + + main_py = '''\ +import sys + +def main(): + print(repr(sys.argv[1:])) + print('hello hello') + return 0 +''' + tmp_path.joinpath('setup.py').write_text(setup_py) + tmp_path.joinpath('socks.py').write_text(main_py) + + +def test_simple_python_hook(tmp_path): + _make_hello_hello(tmp_path) + + ret = run_language(tmp_path, python, 'socks', [os.devnull]) + assert ret == (0, f'[{os.devnull!r}]\nhello hello\n'.encode()) + + +def test_simple_python_hook_default_version(tmp_path): + # make sure that this continues to work for platforms where default + # language detection does not work + with mock.patch.object( + python, + 'get_default_version', + return_value=C.DEFAULT, + ): + test_simple_python_hook(tmp_path) + + +def test_python_hook_weird_setup_cfg(tmp_path): + _make_hello_hello(tmp_path) + setup_cfg = '[install]\ninstall_scripts=/usr/sbin' + tmp_path.joinpath('setup.cfg').write_text(setup_cfg) + + ret = run_language(tmp_path, python, 'socks', [os.devnull]) + assert ret == (0, f'[{os.devnull!r}]\nhello hello\n'.encode()) diff --git a/tests/repository_test.py b/tests/repository_test.py index 045656689..b8dde99b4 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -82,35 +82,6 @@ def _test_hook_repo( assert out == expected -def test_python_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'python_hooks_repo', - 'foo', [os.devnull], - f'[{os.devnull!r}]\nHello World\n'.encode(), - ) - - -def test_python_hook_default_version(tempdir_factory, store): - # make sure that this continues to work for platforms where default - # language detection does not work - with mock.patch.object( - python, - 'get_default_version', - return_value=C.DEFAULT, - ): - test_python_hook(tempdir_factory, store) - - -def test_python_hook_weird_setup_cfg(in_git_dir, tempdir_factory, store): - in_git_dir.join('setup.cfg').write('[install]\ninstall_scripts=/usr/sbin') - - _test_hook_repo( - tempdir_factory, store, 'python_hooks_repo', - 'foo', [os.devnull], - f'[{os.devnull!r}]\nHello World\n'.encode(), - ) - - def test_python_venv_deprecation(store, caplog): config = { 'repo': 'local', From 7a7772fcdae8694107b9ab19cc93ff5fdc690755 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 14 Mar 2023 03:19:10 +0000 Subject: [PATCH 85/91] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.0.1 → v1.1.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.0.1...v1.1.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0aa2e9ea2..cc96a7037 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.0.1 + rev: v1.1.1 hooks: - id: mypy additional_dependencies: [types-all] From a412e5492da8cdac6642b50cc3907db06edec109 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 17 Mar 2023 12:55:34 -0400 Subject: [PATCH 86/91] don't set CARGO_HOME in rust this adds a 270 MB registry cache in the output --- pre_commit/languages/rust.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index af5f483d3..a1f4dbe16 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -50,7 +50,6 @@ def _rust_toolchain(language_version: str) -> str: def get_env_patch(target_dir: str, version: str) -> PatchesT: return ( - ('CARGO_HOME', target_dir), ('PATH', (os.path.join(target_dir, 'bin'), os.pathsep, Var('PATH'))), # Only set RUSTUP_TOOLCHAIN if we don't want use the system's default # toolchain From df2cada973da6ee689cbc8e323caccf5c00df92c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 17 Mar 2023 14:26:34 -0400 Subject: [PATCH 87/91] v3.2.0 --- CHANGELOG.md | 15 +++++++++++++++ setup.cfg | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfcef4530..f2466e207 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +3.2.0 - 2023-03-17 +================== + +### Features +- Allow `pre-commit`, `pre-push`, and `pre-merge-commit` as `stages`. + - #2732 issue by @asottile. + - #2808 PR by @asottile. +- Add `pre-rebase` hook support. + - #2582 issue by @BrutalSimplicity. + - #2725 PR by @mgaligniana. + +### Fixes +- Remove bulky cargo cache from `language: rust` installs. + - #2820 PR by @asottile. + 3.1.1 - 2023-02-27 ================== diff --git a/setup.cfg b/setup.cfg index 507c0ad13..5b3d1560e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.1.1 +version = 3.2.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From ee71a9345ce96a78e011c9635a61abc332e38961 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 25 Mar 2023 13:06:22 -0400 Subject: [PATCH 88/91] set CARGO_HOME while executing rustup --- pre_commit/languages/rust.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index a1f4dbe16..7eec0e7d6 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -80,9 +80,9 @@ def _add_dependencies( lang_base.setup_cmd(prefix, ('cargo', 'add', *crates)) -def install_rust_with_toolchain(toolchain: str) -> None: +def install_rust_with_toolchain(toolchain: str, envdir: str) -> None: with tempfile.TemporaryDirectory() as rustup_dir: - with envcontext((('RUSTUP_HOME', rustup_dir),)): + with envcontext((('CARGO_HOME', envdir), ('RUSTUP_HOME', rustup_dir))): # acquire `rustup` if not present if parse_shebang.find_executable('rustup') is None: # We did not detect rustup and need to download it first. @@ -145,7 +145,7 @@ def install_environment( ctx.enter_context(in_env(prefix, version)) if version != 'system': - install_rust_with_toolchain(_rust_toolchain(version)) + install_rust_with_toolchain(_rust_toolchain(version), envdir) tmpdir = ctx.enter_context(tempfile.TemporaryDirectory()) ctx.enter_context(envcontext((('RUSTUP_HOME', tmpdir),))) From bb49560dc99a65608c8f9161dd71467af163c0d1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 25 Mar 2023 14:02:57 -0400 Subject: [PATCH 89/91] v3.2.1 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2466e207..dfb8f804f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +3.2.1 - 2023-03-25 +================== + +### Fixes +- Fix `language_version` for `language: rust` without global `rustup`. + - #2823 issue by @daschuer. + - #2827 PR by @asottile. + 3.2.0 - 2023-03-17 ================== diff --git a/setup.cfg b/setup.cfg index 5b3d1560e..350fe237a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.2.0 +version = 3.2.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 84f040f58a5710c2a9d6530f9d1e033657665f20 Mon Sep 17 00:00:00 2001 From: Eric DeLabar Date: Mon, 3 Apr 2023 15:50:55 -0400 Subject: [PATCH 90/91] fix #2235 --- pre_commit/languages/swift.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 8250ab703..f16bb0451 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -44,7 +44,7 @@ def install_environment( os.mkdir(envdir) cmd_output_b( 'swift', 'build', - '-C', prefix.prefix_dir, + '--package-path', prefix.prefix_dir, '-c', BUILD_CONFIG, '--build-path', os.path.join(envdir, BUILD_DIR), ) From 5027592625f8df286dea831e84e7bf83021b7c1b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 3 Apr 2023 16:31:09 -0400 Subject: [PATCH 91/91] v3.2.2 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfb8f804f..efd96c796 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +3.2.2 - 2023-04-03 +================== + +### Fixes +- Fix support for swift >= 5.8. + - #2836 PR by @edelabar. + - #2835 issue by @kgrobelny-intive. + 3.2.1 - 2023-03-25 ================== diff --git a/setup.cfg b/setup.cfg index 350fe237a..89e8e4ada 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.2.1 +version = 3.2.2 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown