diff options
107 files changed, 1799 insertions, 2347 deletions
diff --git a/.github/actions/pre-test/action.yml b/.github/actions/pre-test/action.yml new file mode 100644 index 0000000..608c0cd --- /dev/null +++ b/.github/actions/pre-test/action.yml @@ -0,0 +1,40 @@ +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-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.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 0000000..c78d105 --- /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/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7e58bdd..b7d7f1f 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/CHANGELOG.md b/CHANGELOG.md index cd0de5f..c0657e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,54 @@ +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 +================== + +### Fixes +- Ensure coursier hooks are available offline after install. + - #2723 PR by @asottile. + +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 =================== @@ -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 34c94f5..0000000 --- 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: [py37] - 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: [py37] - 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: [py37, py38, py39] - 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/pre_commit/clientlib.py b/pre_commit/clientlib.py index da6ca2b..e191d3a 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,14 +12,9 @@ import cfgv 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 +from pre_commit.yaml import yaml_load logger = logging.getLogger('pre_commit') @@ -35,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( @@ -44,14 +43,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,25 +88,11 @@ load_manifest = functools.partial( ) -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' -# 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 +148,6 @@ class OptionalSensibleRegexAtTop(cfgv.OptionalNoDefault): ) -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 +271,11 @@ CONFIG_REPO_DICT = cfgv.Map( '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), ) @@ -391,35 +335,9 @@ 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, ) - - -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/autoupdate.py b/pre_commit/commands/autoupdate.py index d5352e5..7ed6e77 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -2,6 +2,7 @@ from __future__ import annotations import os.path import re +import tempfile from typing import Any from typing import NamedTuple from typing import Sequence @@ -19,9 +20,8 @@ from pre_commit.store import Store from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b -from pre_commit.util import tmpdir -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): @@ -47,7 +47,7 @@ class RevInfo(NamedTuple): '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/migrate_config.py b/pre_commit/commands/migrate_config.py index c3d0a50..6f7af4e 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -3,9 +3,11 @@ from __future__ import annotations import re import textwrap +import cfgv import yaml -from pre_commit.util import yaml_load +from pre_commit.clientlib import InvalidConfigError +from pre_commit.yaml import yaml_load def _is_header_line(line: str) -> bool: @@ -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/pre_commit/commands/run.py b/pre_commit/commands/run.py index 429e04c..e44e703 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -189,7 +189,16 @@ 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.prefix, + hook.entry, + hook.args, + filenames, + is_local=hook.src == 'local', + 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/commands/try_repo.py b/pre_commit/commands/try_repo.py index ef099f5..539ed3c 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse import logging import os.path +import tempfile import pre_commit.constants as C from pre_commit import git @@ -11,9 +12,8 @@ from pre_commit.clientlib import load_manifest from pre_commit.commands.run import run from pre_commit.store import Store from pre_commit.util import cmd_output_b -from pre_commit.util import tmpdir -from pre_commit.util import yaml_dump from pre_commit.xargs import xargs +from pre_commit.yaml import yaml_dump logger = logging.getLogger(__name__) @@ -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/commands/validate_config.py b/pre_commit/commands/validate_config.py index 91bb017..24bd313 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 372a638..419031a 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/pre_commit/constants.py b/pre_commit/constants.py index 5bc4ae9..3f03cee 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -1,21 +1,14 @@ 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' -# Bump when installation changes in a backwards / forwards incompatible way -INSTALLED_STATE_VERSION = '1' # 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/pre_commit/git.py b/pre_commit/git.py index a76118f..333dc7b 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/hook.py b/pre_commit/hook.py index 202abb3..6d436ca 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 @@ -38,10 +37,6 @@ class Hook(NamedTuple): verbose: bool @property - def cmd(self) -> tuple[str, ...]: - return (*shlex.split(self.entry), *self.args) - - @property def install_key(self) -> tuple[Prefix, str, str, tuple[str, ...]]: return ( self.prefix, diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index cfd42ce..d952ae1 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -1,10 +1,9 @@ from __future__ import annotations -from typing import Callable -from typing import NamedTuple +from typing import ContextManager +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 @@ -27,44 +26,74 @@ from pre_commit.languages import system 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: + ... + + # 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 - run_hook: Callable[[Hook, Sequence[str], bool], tuple[int, bytes]] + 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]: + ... -# 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/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index f0195e4..e2fb019 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -10,15 +10,14 @@ from pre_commit.envcontext import PatchesT from pre_commit.envcontext import SubstitutionT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix -from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b 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: @@ -41,12 +40,8 @@ def get_env_patch(env: str) -> PatchesT: @contextlib.contextmanager -def in_env( - prefix: Prefix, - language_version: str, -) -> Generator[None, None, None]: - directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version) - envdir = prefix.path(directory) +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 @@ -66,31 +61,16 @@ 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) - with clean_path_on_failure(env_dir): + 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, + ) + 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( - 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 - 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/coursier.py b/pre_commit/languages/coursier.py index 9fe43eb..6075758 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -1,81 +1,76 @@ 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.hook import Hook +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 -from pre_commit.util import clean_path_on_failure ENVIRONMENT_DIR = 'coursier' get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check +run_hook = helpers.basic_run_hook 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') - 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 = 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', + envdir = helpers.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)) + + 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', - f'--channel={channel}', + '--channel', channel, app, - f'--dir={envdir}', - ), + ) + elif not additional_dependencies: + raise FatalError( + 'expected .pre-commit-channel dir or additional_dependencies', ) + if additional_dependencies: + _install(*additional_dependencies) -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'))), + ('COURSIER_CACHE', os.path.join(target_dir, '.cs-cache')), ) @contextlib.contextmanager -def in_env( - prefix: Prefix, -) -> Generator[None, None, None]: # pragma: win32 no cover - target_dir = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, get_default_version()), - ) - with envcontext(get_env_patch(target_dir)): +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 - with in_env(hook.prefix): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py index 55ecbf4..e3c1c58 100644 --- a/pre_commit/languages/dart.py +++ b/pre_commit/languages/dart.py @@ -7,21 +7,19 @@ import tempfile 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 -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 +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 def get_env_patch(venv: str) -> PatchesT: @@ -31,9 +29,8 @@ 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) +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 @@ -45,7 +42,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: @@ -67,44 +64,34 @@ def install_environment( 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}', - ) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - with in_env(hook.prefix): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + # 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}', + ) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index eea9f76..e80c959 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -5,18 +5,16 @@ import json 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 from pre_commit.util import CalledProcessError -from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'docker' PRE_COMMIT_LABEL = 'PRE_COMMIT' 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: @@ -95,15 +93,12 @@ 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, version) # 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 @@ -127,16 +122,26 @@ 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], + *, + is_local: bool, + 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 daa4d1b..8e5f2c0 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -2,20 +2,31 @@ from __future__ import annotations 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 health_check = helpers.basic_health_check install_environment = helpers.no_install +in_env = helpers.no_env def run_hook( - hook: Hook, + prefix: Prefix, + entry: str, + args: Sequence[str], file_args: Sequence[str], + *, + is_local: bool, + 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 e26b45c..4c3955e 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -9,20 +9,18 @@ import zipfile 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 -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' 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: @@ -32,9 +30,8 @@ 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) +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 @@ -63,66 +60,56 @@ 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)) - 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) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - with in_env(hook.prefix): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + 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) diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index 00b06a9..33df067 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -2,20 +2,26 @@ from __future__ import annotations 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 health_check = helpers.basic_health_check install_environment = helpers.no_install +in_env = helpers.no_env def run_hook( - hook: Hook, + prefix: Prefix, + entry: str, + args: Sequence[str], file_args: Sequence[str], + *, + is_local: bool, + 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 a5f9dba..3c4b652 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -1,58 +1,129 @@ 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 -from pre_commit import git from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix -from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output -from pre_commit.util import cmd_output_b from pre_commit.util import rmtree ENVIRONMENT_DIR = 'golangenv' -get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check +run_hook = helpers.basic_run_hook +_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'), + ), + ), ) -@contextlib.contextmanager -def in_env(prefix: Prefix) -> Generator[None, None, None]: - envdir = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), - ) - with envcontext(get_env_patch(envdir)): - yield +@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 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 +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: - return 'unknown_src_dir' + 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, version: str) -> Generator[None, None, None]: + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + with envcontext(get_env_patch(envdir, version)): + yield def install_environment( @@ -60,42 +131,29 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - helpers.assert_version_default('golang', version) - directory = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), - ) + env_dir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) - 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)) + if version != 'system': + _install_go(version, env_dir) - # 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( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - with in_env(hook.prefix): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + 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: + 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/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 0be08b5..d1be409 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -1,17 +1,18 @@ from __future__ import annotations +import contextlib import multiprocessing import os import random import re +import shlex from typing import Any +from typing import Generator from typing import NoReturn -from typing import overload from typing import Sequence 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 @@ -48,17 +49,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(prefix: Prefix, d: str, language_version: str) -> str: + return prefix.path(f'{d}-{language_version}') def assert_version_default(binary: str, version: str) -> None: @@ -94,11 +86,16 @@ 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: - 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. @@ -122,13 +119,40 @@ 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]: + 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], + *, + is_local: bool, + 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) + 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 49aa730..ffc40b5 100644 --- a/pre_commit/languages/lua.py +++ b/pre_commit/languages/lua.py @@ -6,19 +6,17 @@ import sys 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 -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' 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 @@ -45,14 +43,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))): +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 @@ -63,29 +57,19 @@ def install_environment( ) -> None: # pragma: win32 no cover 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) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: # pragma: win32 no cover - with in_env(hook.prefix): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + 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) + + # 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) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 37a5b63..9688da3 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -12,16 +12,15 @@ from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.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 ENVIRONMENT_DIR = 'node_env' +run_hook = helpers.basic_run_hook @functools.lru_cache(maxsize=1) @@ -37,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) @@ -65,11 +59,9 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager -def in_env( - prefix: Prefix, - language_version: str, -) -> Generator[None, None, None]: - with envcontext(get_env_patch(_envdir(prefix, 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 @@ -85,47 +77,34 @@ 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) + 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 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) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + 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) diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py index 78bd65a..2530c0e 100644 --- a/pre_commit/languages/perl.py +++ b/pre_commit/languages/perl.py @@ -9,19 +9,13 @@ 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.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 health_check = helpers.basic_health_check - - -def _envdir(prefix: Prefix, version: str) -> str: - directory = helpers.environment_dir(ENVIRONMENT_DIR, version) - return prefix.path(directory) +run_hook = helpers.basic_run_hook def get_env_patch(venv: str) -> PatchesT: @@ -39,11 +33,9 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager -def in_env( - prefix: Prefix, - language_version: str, -) -> Generator[None, None, None]: - with envcontext(get_env_patch(_envdir(prefix, 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 @@ -52,17 +44,7 @@ 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), - ) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + with in_env(prefix, version): + helpers.run_setup_cmd( + prefix, ('cpan', '-T', '.', *additional_dependencies), + ) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 2e2072b..f0eb9a9 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -8,14 +8,15 @@ from typing import Pattern 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 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: @@ -87,12 +88,17 @@ FNS = { def run_hook( - hook: Hook, + prefix: Prefix, + entry: str, + args: Sequence[str], file_args: Sequence[str], + *, + is_local: bool, + 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 19fa247..c373646 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -12,17 +12,16 @@ from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.parse_shebang import find_executable from pre_commit.prefix import Prefix 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 ENVIRONMENT_DIR = 'py_env' +run_hook = helpers.basic_run_hook @functools.lru_cache(maxsize=None) @@ -153,19 +152,14 @@ def norm_version(version: str) -> str | None: @contextlib.contextmanager -def in_env( - prefix: Prefix, - language_version: str, -) -> Generator[None, None, None]: - directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version) - envdir = prefix.path(directory) +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 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 @@ -208,23 +202,13 @@ 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: 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) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + cmd_output_b(*venv_cmd, cwd='/') + with in_env(prefix, version): + helpers.run_setup_cmd(prefix, install_cmd) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index d281102..e238365 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -10,10 +10,8 @@ from typing import Sequence 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 clean_path_on_failure from pre_commit.util import cmd_output_b from pre_commit.util import win_exe @@ -31,32 +29,22 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager -def in_env( - prefix: Prefix, - language_version: str, -) -> Generator[None, None, None]: - envdir = _get_env_dir(prefix, 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 -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, +def _prefix_if_file_entry( + entry: list[str], + prefix: Prefix, + *, + is_local: bool, ) -> Sequence[str]: - if entry[1] == '-e': + if entry[1] == '-e' or is_local: 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 +55,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 @@ -81,20 +69,23 @@ def _entry_validate(entry: Sequence[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`', ) -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], + *, + is_local: bool, +) -> tuple[str, ...]: + cmd = shlex.split(entry) + _entry_validate(cmd) - return ( - *entry[:1], *RSCRIPT_OPTS, - *_prefix_if_non_local_file_entry(entry, hook.prefix, hook.src), - *hook.args, - ) + cmd_part = _prefix_if_file_entry(cmd, prefix, is_local=is_local) + return (cmd[0], *RSCRIPT_OPTS, *cmd_part, *args) def install_environment( @@ -102,55 +93,54 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - env_dir = _get_env_dir(prefix, version) - with clean_path_on_failure(env_dir): - os.makedirs(env_dir, exist_ok=True) - 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, + 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')) + + 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: @@ -166,11 +156,19 @@ def _inline_r_setup(code: str) -> str: def run_hook( - hook: Hook, + prefix: Prefix, + entry: str, + args: Sequence[str], file_args: Sequence[str], + *, + is_local: bool, + require_serial: bool, 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, - ) + cmd = _cmd_from_hook(prefix, entry, args, is_local=is_local) + return helpers.run_xargs( + cmd, + file_args, + require_serial=require_serial, + color=color, + ) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 8955dd0..b4d4b45 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -13,15 +13,14 @@ from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.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' health_check = helpers.basic_health_check +run_hook = helpers.basic_run_hook @functools.lru_cache(maxsize=1) @@ -40,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 += ( @@ -68,14 +68,9 @@ def get_env_patch( @contextlib.contextmanager -def in_env( - prefix: Prefix, - language_version: str, -) -> Generator[None, None, None]: - envdir = prefix.path( - helpers.environment_dir(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 @@ -89,14 +84,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) @@ -115,39 +110,27 @@ 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, - ), - ) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + # 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, + ), + ) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 204f2aa..391fd86 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -15,16 +15,15 @@ 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.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 ENVIRONMENT_DIR = 'rustenv' health_check = helpers.basic_health_check +run_hook = helpers.basic_run_hook @functools.lru_cache(maxsize=1) @@ -49,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), @@ -68,13 +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]: - with envcontext( - get_env_patch(_envdir(prefix, language_version), 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 @@ -126,7 +116,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 @@ -143,34 +133,24 @@ 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,)) - - 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) + 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,)) - for args in packages_to_install: - cmd_output_b( - 'cargo', 'install', '--bins', '--root', directory, *args, - cwd=prefix.prefix_dir, - ) + with in_env(prefix, version): + if version != 'system': + install_rust_with_toolchain(_rust_toolchain(version)) + if len(lib_deps) > 0: + _add_dependencies(prefix, lib_deps) -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + for args in packages_to_install: + cmd_output_b( + 'cargo', 'install', '--bins', '--root', envdir, *args, + cwd=prefix.prefix_dir, + ) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index d5e7677..08325f4 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -2,19 +2,31 @@ from __future__ import annotations 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 health_check = helpers.basic_health_check install_environment = helpers.no_install +in_env = helpers.no_env def run_hook( - hook: Hook, + prefix: Prefix, + entry: str, + args: Sequence[str], file_args: Sequence[str], + *, + is_local: bool, + 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 4c68703..c66ad5f 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -5,21 +5,20 @@ import os 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 -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 +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 @@ -28,10 +27,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 = prefix.path( - helpers.environment_dir(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 @@ -41,25 +38,13 @@ 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, version) # 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), - ) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: # pragma: win32 no cover - with in_env(hook.prefix): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + os.mkdir(envdir) + cmd_output_b( + 'swift', 'build', + '-C', prefix.prefix_dir, + '-c', BUILD_CONFIG, + '--build-path', os.path.join(envdir, BUILD_DIR), + ) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index c64fb36..204cad7 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -1,20 +1,10 @@ from __future__ import annotations -from typing import Sequence - -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 - - -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) +in_env = helpers.no_env +run_hook = helpers.basic_run_hook diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index 3ac933c..3ee04e8 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 normexe(orig: str) -> str: 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/repository.py b/pre_commit/repository.py index 4092277..616faf5 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -10,28 +10,33 @@ import pre_commit.constants as C 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 clean_path_on_failure from pre_commit.util import rmtree 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(prefix: Prefix, venv: str) -> str: - return prefix.path(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(prefix: Prefix, venv: str) -> object | None: - filename = _state_filename(prefix, venv) + +def _read_state(venv: str) -> object | None: + filename = _state_filename_v1(venv) if not os.path.exists(filename): return None else: @@ -39,26 +44,22 @@ 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] - venv = environment_dir(lang.ENVIRONMENT_DIR, hook.language_version) + if lang.ENVIRONMENT_DIR is None: + return True + + venv = environment_dir( + hook.prefix, + 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) - ) + ( + 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) ) @@ -69,26 +70,41 @@ 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 = environment_dir( + hook.prefix, + lang.ENVIRONMENT_DIR, + hook.language_version, + ) # 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}', + ) + + # 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_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( diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz Binary files differindex 35419f6..b6eacf5 100644 --- a/pre_commit/resources/ruby-build.tar.gz +++ b/pre_commit/resources/ruby-build.tar.gz diff --git a/pre_commit/store.py b/pre_commit/store.py index effebfb..6ddc7c4 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,37 +205,9 @@ class Store: 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) - - 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, '<<unknown>>') - _git_cmd('add', '.') - git.commit(repo=directory) - 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/pre_commit/util.py b/pre_commit/util.py index b850768..8ea4844 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -2,36 +2,20 @@ from __future__ import annotations import contextlib import errno -import functools import importlib.resources import os.path import shutil import stat import subprocess import sys -import tempfile from types import TracebackType from typing import Any from typing import Callable 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): @@ -52,18 +36,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) @@ -127,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: @@ -254,10 +226,5 @@ def rmtree(path: str) -> None: 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/pre_commit/yaml.py b/pre_commit/yaml.py new file mode 100644 index 0000000..bdf4ec4 --- /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, + ) @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.21.0 +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 @@ -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 = @@ -35,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/testing/fixtures.py b/testing/fixtures.py index 5182a08..79a1160 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -12,8 +12,8 @@ from pre_commit import git 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/testing/gen-languages-all b/testing/gen-languages-all deleted file mode 100755 index 05f8929..0000000 --- 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/testing/get-coursier.ps1 b/testing/get-coursier.ps1 deleted file mode 100755 index 42e5635..0000000 --- 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 4c5e955..958e73b 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 '##vso[task.prependpath]/tmp/coursier' +mkdir -p /tmp/coursier +curl --location --silent --output "$TARGET" "$URL" +echo "$SHA256 $TARGET" | sha256sum --check +unpack diff --git a/testing/get-dart.sh b/testing/get-dart.sh index b655e1a..998b9d9 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 580e247..0000000 --- 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-r.ps1 b/testing/get-r.ps1 deleted file mode 100644 index e7b7b61..0000000 --- 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 5d09828..0000000 --- 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)' diff --git a/testing/get-swift.sh b/testing/get-swift.sh index 3e78082..dfe0939 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" diff --git a/testing/language_helpers.py b/testing/language_helpers.py new file mode 100644 index 0000000..f9ae0b1 --- /dev/null +++ b/testing/language_helpers.py @@ -0,0 +1,35 @@ +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] = (), + is_local: bool = False, +) -> 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, + is_local=is_local, + require_serial=True, + color=False, + ) + out = out.replace(b'\r\n', b'\n') + return ret, out diff --git a/testing/make-archives b/testing/make-archives index 704101f..cec9a9f 100755 --- a/testing/make-archives +++ b/testing/make-archives @@ -17,7 +17,7 @@ from typing import Sequence REPOS = ( ('rbenv', 'https://github.com/rbenv/rbenv', '38e1fbb'), - ('ruby-build', 'https://github.com/rbenv/ruby-build', '98c0337'), + ('ruby-build', 'https://github.com/rbenv/ruby-build', '9d92a69'), ( 'ruby-download', 'https://github.com/garnieretienne/rvm-download', 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 a0d274c..0000000 --- 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 e23c079..0000000 --- a/testing/resources/conda_hooks_repo/environment.yml +++ /dev/null @@ -1,6 +0,0 @@ -channels: - - conda-forge - - defaults -dependencies: - - python - - pip 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 37f401e..0000000 --- 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 d4a143b..0000000 --- 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/resources/dart_repo/.pre-commit-hooks.yaml b/testing/resources/dart_repo/.pre-commit-hooks.yaml deleted file mode 100644 index e0dc5a2..0000000 --- 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 5d8d6a6..0000000 --- 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 bc719d0..0000000 --- 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/testing/resources/golang_hooks_repo/golang-hello-world/main.go b/testing/resources/golang_hooks_repo/golang-hello-world/main.go index 1e3c591..1685743 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/testing/resources/lua_repo/.pre-commit-hooks.yaml b/testing/resources/lua_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 767ef97..0000000 --- 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 2a0e002..0000000 --- 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 82486e0..0000000 --- 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/resources/perl_hooks_repo/.gitignore b/testing/resources/perl_hooks_repo/.gitignore deleted file mode 100644 index 7af9940..0000000 --- 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 11e6f6c..0000000 --- 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 4a20084..0000000 --- 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 6c70e10..0000000 --- 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 9474009..0000000 --- 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 c76521c..0000000 --- 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/testing/resources/r_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/r_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index b3545d9..0000000 --- a/testing/resources/r_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,48 +0,0 @@ -# parsing file -- id: parse-file-no-opts-no-args - name: Say hi - entry: Rscript parse-file-no-opts-no-args.R - language: r - types: [r] -- id: parse-file-no-opts-args - name: Say hi - entry: Rscript parse-file-no-opts-args.R - args: [--no-cache] - language: r - types: [r] -## parsing expr -- id: parse-expr-no-opts-no-args-1 - name: Say hi - entry: Rscript -e '1+1' - language: r - types: [r] -- id: parse-expr-args-in-entry-2 - name: Say hi - entry: Rscript -e '1+1' -e '3' --no-cache3 - language: r - types: [r] -# real world -- id: hello-world - name: Say hi - entry: Rscript hello-world.R - args: [blibla] - language: r - types: [r] -- id: hello-world-inline - name: Say hi - entry: | - Rscript -e - 'stopifnot( - packageVersion("rprojroot") == "1.0", - packageVersion("gli.clu") == "0.0.0.9000" - ) - cat(commandArgs(trailingOnly = TRUE), "from R!\n", sep = ", ") - ' - args: ['Hi-there'] - language: r - types: [r] -- id: additional-deps - name: Check additional deps - entry: Rscript additional-deps.R - language: r - types: [r] diff --git a/testing/resources/r_hooks_repo/DESCRIPTION b/testing/resources/r_hooks_repo/DESCRIPTION deleted file mode 100644 index 0e597a8..0000000 --- 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 bc14595..0000000 --- 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 bf8d92f..0000000 --- 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 d7d5fdc..0000000 --- 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 253c5d1..0000000 --- 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 d8d092c..0000000 --- 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/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml index 364d47d..c97939a 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/testing/resources/swift_hooks_repo/.gitignore b/testing/resources/swift_hooks_repo/.gitignore deleted file mode 100644 index 02c0875..0000000 --- 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 c08df87..0000000 --- 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 04976d3..0000000 --- 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 f7cf60e..0000000 --- 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 e807f04..b6c3804 100644 --- a/testing/util.py +++ b/testing/util.py @@ -6,7 +6,6 @@ import subprocess 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 @@ -42,22 +41,10 @@ def cmd_output_mocked_pre_commit_home( return ret, out.replace('\r\n', '\n'), None -skipif_cant_run_coursier = pytest.mark.skipif( - os.name == 'nt' or parse_shebang.find_executable('cs') is None, - reason="coursier isn't installed or can't be found", -) skipif_cant_run_docker = pytest.mark.skipif( os.name == 'nt' or not docker_is_running(), reason="Docker isn't running or can't be accessed", ) -skipif_cant_run_lua = pytest.mark.skipif( - os.name == 'nt', - reason="lua isn't installed or can't be found", -) -skipif_cant_run_swift = pytest.mark.skipif( - parse_shebang.find_executable('swift') is None, - reason="swift isn't installed or can't be found", -) xfailif_windows = pytest.mark.xfail(os.name == 'nt', reason='windows') diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index b4c3c4e..efb2aa8 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -14,11 +14,9 @@ from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import DEFAULT_LANGUAGE_VERSION from pre_commit.clientlib import MANIFEST_SCHEMA from pre_commit.clientlib import META_HOOK_DICT -from pre_commit.clientlib import MigrateShaToRev from pre_commit.clientlib import OptionalSensibleRegexAtHook from pre_commit.clientlib import OptionalSensibleRegexAtTop -from pre_commit.clientlib import validate_config_main -from pre_commit.clientlib import validate_manifest_main +from pre_commit.clientlib import parse_version from testing.fixtures import sample_local_config @@ -112,78 +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_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( - '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']}, @@ -370,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'), ( @@ -426,48 +340,6 @@ def test_valid_manifests(manifest_obj, 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', ( # i-dont-exist isn't a valid hook @@ -513,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/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 3806b0e..4bcb5d8 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -4,12 +4,11 @@ import shlex 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) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 379c03a..a1ecda8 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. ' @@ -739,20 +740,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 +768,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 +812,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 +843,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', '.') diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index b80244e..fca1ad9 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) diff --git a/tests/commands/validate_config_test.py b/tests/commands/validate_config_test.py new file mode 100644 index 0000000..a475cd8 --- /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 0000000..a4bc8ac --- /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,)) diff --git a/tests/languages/conda_test.py b/tests/languages/conda_test.py index 5023b2a..83aaebe 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 @@ from pre_commit.languages.conda import _conda_exe ) 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/languages/coursier_test.py b/tests/languages/coursier_test.py new file mode 100644 index 0000000..dbb746c --- /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/languages/dart_test.py b/tests/languages/dart_test.py new file mode 100644 index 0000000..5bb5aa6 --- /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/languages/golang_test.py b/tests/languages/golang_test.py index 9e393cb..0219261 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -1,22 +1,43 @@ from __future__ import annotations +import re +from unittest import mock + 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 +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/languages/helpers_test.py b/tests/languages/helpers_test.py index f333e79..c209e7e 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -12,7 +12,6 @@ from pre_commit import parse_shebang from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError -from testing.auto_namedtuple import auto_namedtuple @pytest.fixture @@ -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/lua_test.py b/tests/languages/lua_test.py new file mode 100644 index 0000000..b2767b7 --- /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/languages/perl_test.py b/tests/languages/perl_test.py new file mode 100644 index 0000000..042478d --- /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/languages/r_test.py b/tests/languages/r_test.py index c52d5ac..02c559c 100644 --- a/tests/languages/r_test.py +++ b/tests/languages/r_test.py @@ -1,136 +1,119 @@ 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.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={}, - expect_path_prefix=True, -): - repo_path = 'r_hooks_repo' - path = make_repo(tempdir_factory, repo_path) - config = config or make_config_from_repo(path) - hook = _get_hook_no_install(config, store, hook_id) - ret = r._cmd_from_hook(hook) - expected_cmd = 'Rscript' - 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 = ( - expected_cmd, - *expected_opts, - *(expected_hook_expr or (expected_path,)), - *expected_args, - ) - assert ret == expected +from testing.language_helpers import run_language -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_no_opts_no_args(tmp_path): + 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', + str(tmp_path.joinpath('some-script.R')), + ) -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(tmp_path): + cmd = r._cmd_from_hook( + Prefix(str(tmp_path)), + 'Rscript some-script.R', + ('--no-cache',), + is_local=False, + ) + 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_file_no_opts_args(tempdir_factory, store): - hook_id = 'parse-file-no-opts-args' - expected_args = ['--no-cache'] - _test_r_parsing( - tempdir_factory, store, hook_id, expected_args=expected_args, +def test_r_parsing_expr_no_opts_no_args1(tmp_path): + 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', + '-e', '1+1', ) -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_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(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', - ], + ['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): - path = 'path/to/script.R' - hook_id = 'local-r' - config = { - 'repo': 'local', - 'hooks': [{ - 'id': hook_id, - 'name': 'local-r', - 'entry': f'Rscript {path}', - 'language': 'r', - }], - } - _test_r_parsing( - tempdir_factory, - store, - hook_id=hook_id, - expected_hook_expr=(path,), - config=config, - expect_path_prefix=False, - ) + msg, = excinfo.value.args + assert msg == 'entry must start with `Rscript`.' def test_rscript_exec_relative_to_r_home(): @@ -142,3 +125,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/languages/ruby_test.py b/tests/languages/ruby_test.py index 29f3c80..63a16eb 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/languages/rust_test.py b/tests/languages/rust_test.py index f011e71..b8167a9 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 test_installs_with_bootstrapped_rustup(tmpdir, language_version): 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/languages/swift_test.py b/tests/languages/swift_test.py new file mode 100644 index 0000000..e0a8ea4 --- /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/parse_shebang_test.py b/tests/parse_shebang_test.py index d7acbf5..2fcb29e 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 diff --git a/tests/repository_test.py b/tests/repository_test.py index c3936bf..85cf458 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -23,6 +23,7 @@ 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 from pre_commit.repository import all_hooks from pre_commit.repository import install_hook_envs from pre_commit.util import cmd_output @@ -32,10 +33,7 @@ from testing.fixtures import make_repo from testing.fixtures import modify_manifest from testing.util import cwd from testing.util import get_resource_path -from testing.util import skipif_cant_run_coursier from testing.util import skipif_cant_run_docker -from testing.util import skipif_cant_run_lua -from testing.util import skipif_cant_run_swift from testing.util import xfailif_windows @@ -44,7 +42,16 @@ 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.prefix, + hook.entry, + hook.args, + filenames, + is_local=hook.src == 'local', + require_serial=hook.require_serial, + color=color, + ) def _get_hook_no_install(repo_config, store, hook_id): @@ -81,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', @@ -133,9 +99,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) @@ -189,15 +157,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( @@ -247,9 +206,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) @@ -267,54 +228,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', @@ -335,7 +248,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', ) @@ -357,7 +270,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', ) @@ -368,25 +281,36 @@ def test_system_hook_with_spaces(tempdir_factory, store): ) -@skipif_cant_run_swift # pragma: win32 no cover -def test_swift_hook(tempdir_factory, store): +def test_golang_system_hook(tempdir_factory, store): _test_hook_repo( - tempdir_factory, store, 'swift_hooks_repo', - 'swift-hooks-repo', [], b'Hello, world!\n', + 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_hook(tempdir_factory, store): +def test_golang_versioned_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'golang_hooks_repo', - 'golang-hook', [], b'hello world\n', + '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) == [] @@ -459,11 +383,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 @@ -478,11 +403,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 @@ -638,6 +564,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) @@ -668,11 +609,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, + 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 @@ -788,10 +730,14 @@ def test_control_c_control_c_on_install(tempdir_factory, store): # 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) @@ -807,10 +753,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__') ] @@ -1001,30 +949,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', ( @@ -1041,46 +965,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', @@ -1125,29 +1009,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 diff --git a/tests/store_test.py b/tests/store_test.py index 8187766..c42ce65 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -9,6 +9,7 @@ import pytest 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): diff --git a/tests/util_test.py b/tests/util_test.py index b3f18b4..310f8f5 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -12,9 +12,7 @@ from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_p from pre_commit.util import make_executable -from pre_commit.util import parse_version from pre_commit.util import rmtree -from pre_commit.util import tmpdir def test_CalledProcessError_str(): @@ -74,12 +72,6 @@ def test_clean_path_on_failure_cleans_for_system_exit(in_tmpdir): 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 @@ -105,12 +97,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() @@ -1,5 +1,5 @@ [tox] -envlist = py37,py38,pypy3,pre-commit +envlist = py,pypy3,pre-commit [testenv] deps = -rrequirements-dev.txt |