From d7784aa0c412c80cfcb33a29fd1b2ea69dbe8ab8 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Mon, 27 Feb 2023 11:32:22 +0100 Subject: Merging upstream version 3.1.0. Signed-off-by: Daniel Baumann --- .github/ISSUE_TEMPLATE/00_bug.yaml | 54 ++++ .github/ISSUE_TEMPLATE/01_feature.yaml | 38 +++ .github/ISSUE_TEMPLATE/bug.yaml | 48 --- .github/ISSUE_TEMPLATE/config.yml | 8 + .github/actions/pre-test/action.yml | 31 -- .github/workflows/languages.yaml | 82 +++++ .pre-commit-config.yaml | 2 +- CHANGELOG.md | 18 ++ CONTRIBUTING.md | 6 +- pre_commit/all_languages.py | 48 +++ pre_commit/clientlib.py | 8 +- pre_commit/commands/migrate_config.py | 9 + pre_commit/commands/run.py | 2 +- pre_commit/lang_base.py | 196 ++++++++++++ pre_commit/languages/all.py | 99 ------ pre_commit/languages/conda.py | 14 +- pre_commit/languages/coursier.py | 18 +- pre_commit/languages/dart.py | 20 +- pre_commit/languages/docker.py | 23 +- pre_commit/languages/docker_image.py | 14 +- pre_commit/languages/dotnet.py | 28 +- pre_commit/languages/fail.py | 10 +- pre_commit/languages/golang.py | 16 +- pre_commit/languages/helpers.py | 158 ---------- pre_commit/languages/lua.py | 18 +- pre_commit/languages/node.py | 18 +- pre_commit/languages/perl.py | 14 +- pre_commit/languages/pygrep.py | 10 +- pre_commit/languages/python.py | 14 +- pre_commit/languages/r.py | 12 +- pre_commit/languages/ruby.py | 33 +- pre_commit/languages/rust.py | 12 +- pre_commit/languages/script.py | 14 +- pre_commit/languages/swift.py | 16 +- pre_commit/languages/system.py | 12 +- pre_commit/repository.py | 15 +- pre_commit/staged_files_only.py | 23 +- pre_commit/store.py | 2 +- pre_commit/util.py | 7 +- setup.cfg | 2 +- testing/language_helpers.py | 15 +- testing/languages | 79 +++++ .../docker_hooks_repo/.pre-commit-hooks.yaml | 17 -- testing/resources/docker_hooks_repo/Dockerfile | 3 - .../docker_image_hooks_repo/.pre-commit-hooks.yaml | 8 - .../dotnet_hooks_combo_repo/.pre-commit-hooks.yaml | 12 - .../dotnet_hooks_combo_repo.sln | 28 -- .../dotnet_hooks_combo_repo/proj1/Program.cs | 12 - .../dotnet_hooks_combo_repo/proj1/proj1.csproj | 12 - .../dotnet_hooks_combo_repo/proj2/Program.cs | 12 - .../dotnet_hooks_combo_repo/proj2/proj2.csproj | 12 - .../dotnet_hooks_csproj_prefix_repo/.gitignore | 3 - .../.pre-commit-hooks.yaml | 5 - .../dotnet_hooks_csproj_prefix_repo/Program.cs | 12 - .../dotnet_hooks_csproj_prefix_repo.csproj | 9 - .../resources/dotnet_hooks_csproj_repo/.gitignore | 3 - .../.pre-commit-hooks.yaml | 5 - .../resources/dotnet_hooks_csproj_repo/Program.cs | 12 - .../dotnet_hooks_csproj_repo.csproj | 9 - testing/resources/dotnet_hooks_sln_repo/.gitignore | 3 - .../dotnet_hooks_sln_repo/.pre-commit-hooks.yaml | 5 - testing/resources/dotnet_hooks_sln_repo/Program.cs | 12 - .../dotnet_hooks_sln_repo.csproj | 9 - .../dotnet_hooks_sln_repo.sln | 34 --- .../golang_hooks_repo/.pre-commit-hooks.yaml | 5 - testing/resources/golang_hooks_repo/go.mod | 5 - testing/resources/golang_hooks_repo/go.sum | 2 - .../golang_hooks_repo/golang-hello-world/main.go | 23 -- .../python_venv_hooks_repo/.pre-commit-hooks.yaml | 5 - testing/resources/python_venv_hooks_repo/foo.py | 9 - testing/resources/python_venv_hooks_repo/setup.py | 10 - testing/util.py | 15 - tests/all_languages_test.py | 7 + tests/commands/migrate_config_test.py | 33 ++ tests/lang_base_test.py | 173 +++++++++++ tests/languages/docker_image_test.py | 27 ++ tests/languages/docker_test.py | 14 + tests/languages/dotnet_test.py | 154 ++++++++++ tests/languages/fail_test.py | 14 + tests/languages/golang_test.py | 97 +++++- tests/languages/helpers_test.py | 135 --------- tests/languages/pygrep_test.py | 17 ++ tests/languages/python_test.py | 23 ++ tests/languages/ruby_test.py | 4 +- tests/languages/script_test.py | 14 + tests/languages/system_test.py | 9 + tests/repository_test.py | 331 +++------------------ tests/staged_files_only_test.py | 50 ++++ tests/store_test.py | 26 +- tests/util_test.py | 2 +- tox.ini | 4 +- 91 files changed, 1434 insertions(+), 1277 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/00_bug.yaml create mode 100644 .github/ISSUE_TEMPLATE/01_feature.yaml delete mode 100644 .github/ISSUE_TEMPLATE/bug.yaml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/workflows/languages.yaml create mode 100644 pre_commit/all_languages.py create mode 100644 pre_commit/lang_base.py delete mode 100644 pre_commit/languages/all.py delete mode 100644 pre_commit/languages/helpers.py create mode 100755 testing/languages delete mode 100644 testing/resources/docker_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/docker_hooks_repo/Dockerfile delete mode 100644 testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/dotnet_hooks_combo_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/dotnet_hooks_combo_repo/dotnet_hooks_combo_repo.sln delete mode 100644 testing/resources/dotnet_hooks_combo_repo/proj1/Program.cs delete mode 100644 testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj delete mode 100644 testing/resources/dotnet_hooks_combo_repo/proj2/Program.cs delete mode 100644 testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj delete mode 100644 testing/resources/dotnet_hooks_csproj_prefix_repo/.gitignore delete mode 100644 testing/resources/dotnet_hooks_csproj_prefix_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/dotnet_hooks_csproj_prefix_repo/Program.cs delete mode 100644 testing/resources/dotnet_hooks_csproj_prefix_repo/dotnet_hooks_csproj_prefix_repo.csproj delete mode 100644 testing/resources/dotnet_hooks_csproj_repo/.gitignore delete mode 100644 testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/dotnet_hooks_csproj_repo/Program.cs delete mode 100644 testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj delete mode 100644 testing/resources/dotnet_hooks_sln_repo/.gitignore delete mode 100644 testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/dotnet_hooks_sln_repo/Program.cs delete mode 100644 testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj delete mode 100644 testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.sln delete mode 100644 testing/resources/golang_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/golang_hooks_repo/go.mod delete mode 100644 testing/resources/golang_hooks_repo/go.sum delete mode 100644 testing/resources/golang_hooks_repo/golang-hello-world/main.go delete mode 100644 testing/resources/python_venv_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/python_venv_hooks_repo/foo.py delete mode 100644 testing/resources/python_venv_hooks_repo/setup.py create mode 100644 tests/all_languages_test.py create mode 100644 tests/lang_base_test.py create mode 100644 tests/languages/docker_image_test.py create mode 100644 tests/languages/fail_test.py delete mode 100644 tests/languages/helpers_test.py create mode 100644 tests/languages/script_test.py create mode 100644 tests/languages/system_test.py diff --git a/.github/ISSUE_TEMPLATE/00_bug.yaml b/.github/ISSUE_TEMPLATE/00_bug.yaml new file mode 100644 index 0000000..980f7af --- /dev/null +++ b/.github/ISSUE_TEMPLATE/00_bug.yaml @@ -0,0 +1,54 @@ +name: bug report +description: something went wrong +body: + - type: markdown + attributes: + value: | + this is for issues for `pre-commit` (the framework). + if you are reporting an issue for [pre-commit.ci] please report it at [pre-commit-ci/issues] + + [pre-commit.ci]: https://pre-commit.ci + [pre-commit-ci/issues]: https://github.com/pre-commit-ci/issues + - type: input + id: search + attributes: + label: search you tried in the issue tracker + placeholder: ... + validations: + required: true + - type: markdown + attributes: + value: | + 95% of issues created are duplicates. + please try extra hard to find them first. + it's very unlikely your problem is unique. + - type: textarea + id: freeform + attributes: + label: describe your issue + placeholder: 'I was doing ... I ran ... I expected ... I got ...' + validations: + required: true + - type: input + id: version + attributes: + label: pre-commit --version + placeholder: pre-commit x.x.x + validations: + required: true + - type: textarea + id: configuration + attributes: + label: .pre-commit-config.yaml + description: (auto-rendered as yaml, no need for backticks) + placeholder: 'repos: ...' + render: yaml + validations: + required: true + - type: textarea + id: error-log + attributes: + label: '~/.cache/pre-commit/pre-commit.log (if present)' + placeholder: "### version information\n..." + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/01_feature.yaml b/.github/ISSUE_TEMPLATE/01_feature.yaml new file mode 100644 index 0000000..c7ddc84 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01_feature.yaml @@ -0,0 +1,38 @@ +name: feature request +description: something new +body: + - type: markdown + attributes: + value: | + this is for issues for `pre-commit` (the framework). + if you are reporting an issue for [pre-commit.ci] please report it at [pre-commit-ci/issues] + + [pre-commit.ci]: https://pre-commit.ci + [pre-commit-ci/issues]: https://github.com/pre-commit-ci/issues + - type: input + id: search + attributes: + label: search you tried in the issue tracker + placeholder: ... + validations: + required: true + - type: markdown + attributes: + value: | + 95% of issues created are duplicates. + please try extra hard to find them first. + it's very unlikely your feature idea is a new one. + - type: textarea + id: freeform + attributes: + label: describe your actual problem + placeholder: 'I want to do ... I tried ... It does not work because ...' + validations: + required: true + - type: input + id: version + attributes: + label: pre-commit --version + placeholder: pre-commit x.x.x + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml deleted file mode 100644 index 96cd6c7..0000000 --- a/.github/ISSUE_TEMPLATE/bug.yaml +++ /dev/null @@ -1,48 +0,0 @@ -name: bug report -description: something went wrong -body: - - type: markdown - attributes: - value: | - this is for issues for `pre-commit` (the framework). - if you are reporting an issue for [pre-commit.ci] please report it at [pre-commit-ci/issues] - - [pre-commit.ci]: https://pre-commit.ci - [pre-commit-ci/issues]: https://github.com/pre-commit-ci/issues - - type: input - id: search - attributes: - label: search you tried in the issue tracker - placeholder: ... - validations: - required: true - - type: textarea - id: freeform - attributes: - label: describe your issue - placeholder: 'I was doing ... I ran ... I expected ... I got ...' - validations: - required: true - - type: input - id: version - attributes: - label: pre-commit --version - placeholder: pre-commit x.x.x - validations: - required: true - - type: textarea - id: configuration - attributes: - label: .pre-commit-config.yaml - description: (auto-rendered as yaml, no need for backticks) - placeholder: 'repos: ...' - render: yaml - validations: - required: true - - type: textarea - id: error-log - attributes: - label: '~/.cache/pre-commit/pre-commit.log (if present)' - placeholder: "### version information\n..." - validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..4179f47 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: +- name: documentation + url: https://pre-commit.com + about: please check the docs first +- name: pre-commit.ci issues + url: https://github.com/pre-commit-ci/issues + about: please report issues about pre-commit.ci here diff --git a/.github/actions/pre-test/action.yml b/.github/actions/pre-test/action.yml index 42bbf00..9d1eb2d 100644 --- a/.github/actions/pre-test/action.yml +++ b/.github/actions/pre-test/action.yml @@ -5,36 +5,5 @@ inputs: runs: using: composite steps: - - name: setup (windows) - shell: bash - if: runner.os == 'Windows' - run: | - set -x - - echo 'TEMP=C:\TEMP' >> "$GITHUB_ENV" - - echo "$CONDA\Scripts" >> "$GITHUB_PATH" - - echo 'C:\Strawberry\perl\bin' >> "$GITHUB_PATH" - echo 'C:\Strawberry\perl\site\bin' >> "$GITHUB_PATH" - echo 'C:\Strawberry\c\bin' >> "$GITHUB_PATH" - - testing/get-coursier.sh - testing/get-dart.sh - - name: setup (linux) - shell: bash - if: runner.os == 'Linux' - run: | - set -x - - sudo apt-get update - sudo apt-get install -y --no-install-recommends \ - lua5.3 \ - liblua5.3-dev \ - luarocks - - testing/get-coursier.sh - testing/get-dart.sh - testing/get-swift.sh - uses: asottile/workflows/.github/actions/latest-git@v1.4.0 if: inputs.env == 'py38' && runner.os == 'Linux' diff --git a/.github/workflows/languages.yaml b/.github/workflows/languages.yaml new file mode 100644 index 0000000..8bc8e71 --- /dev/null +++ b/.github/workflows/languages.yaml @@ -0,0 +1,82 @@ +name: languages + +on: + push: + branches: [main, test-me-*] + tags: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + vars: + runs-on: ubuntu-latest + outputs: + languages: ${{ steps.vars.outputs.languages }} + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-python@v4 + with: + python-version: 3.8 + - name: install deps + run: python -mpip install -e . -r requirements-dev.txt + - name: vars + run: testing/languages ${{ github.event_name == 'push' && '--all' || '' }} + id: vars + language: + needs: [vars] + runs-on: ${{ matrix.os }} + if: needs.vars.outputs.languages != '[]' + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON(needs.vars.outputs.languages) }} + steps: + - uses: asottile/workflows/.github/actions/fast-checkout@v1.4.0 + - uses: actions/setup-python@v4 + with: + python-version: 3.8 + + - run: echo "$CONDA\Scripts" >> "$GITHUB_PATH" + shell: bash + if: matrix.os == 'windows-latest' && matrix.language == 'conda' + - run: testing/get-coursier.sh + shell: bash + if: matrix.language == 'coursier' + - run: testing/get-dart.sh + shell: bash + if: matrix.language == 'dart' + - run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + lua5.3 \ + liblua5.3-dev \ + luarocks + if: matrix.os == 'ubuntu-latest' && matrix.language == 'lua' + - run: | + echo 'C:\Strawberry\perl\bin' >> "$GITHUB_PATH" + echo 'C:\Strawberry\perl\site\bin' >> "$GITHUB_PATH" + echo 'C:\Strawberry\c\bin' >> "$GITHUB_PATH" + shell: bash + if: matrix.os == 'windows-latest' && matrix.language == 'perl' + - run: testing/get-swift.sh + if: matrix.os == 'ubuntu-latest' && matrix.language == 'swift' + + - name: install deps + run: python -mpip install -e . -r requirements-dev.txt + - name: run tests + run: coverage run -m pytest tests/languages/${{ matrix.language }}_test.py + - name: check coverage + run: coverage report --include pre_commit/languages/${{ matrix.language }}.py,tests/languages/${{ matrix.language }}_test.py + collector: + needs: [language] + if: always() + runs-on: ubuntu-latest + steps: + - name: check for failures + if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') + run: echo job failed && exit 1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b7d7f1f..ad8ffba 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.991 + rev: v1.0.1 hooks: - id: mypy additional_dependencies: [types-all] diff --git a/CHANGELOG.md b/CHANGELOG.md index 0998da9..8a42781 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +3.1.0 - 2023-02-22 +================== + +### Fixes +- Fix `dotnet` for `.sln`-based hooks for dotnet>=7.0.200. + - #2763 PR by @m-rsha. +- Prevent stashing when `diff` fails to execute. + - #2774 PR by @asottile. + - #2773 issue by @strubbly. +- Dependencies are no longer sorted in repository key. + - #2776 PR by @asottile. + +### Updating +- Deprecate `language: python_venv`. Use `language: python` instead. + - #2746 PR by @asottile. + - #2734 issue by @asottile. + + 3.0.4 - 2023-02-03 ================== diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9bcb79..ab3a929 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,10 +64,10 @@ to implement. The current implemented languages are at varying levels: - 0th class - pre-commit does not require any dependencies for these languages as they're not actually languages (current examples: fail, pygrep) - 1st class - pre-commit will bootstrap a full interpreter requiring nothing to - be installed globally (current examples: node, ruby, rust) + be installed globally (current examples: go, node, ruby, rust) - 2nd class - pre-commit requires the user to install the language globally but - will install tools in an isolated fashion (current examples: python, go, - swift, docker). + will install tools in an isolated fashion (current examples: python, swift, + docker). - 3rd class - pre-commit requires the user to install both the tool and the language globally (current examples: script, system) diff --git a/pre_commit/all_languages.py b/pre_commit/all_languages.py new file mode 100644 index 0000000..2bed706 --- /dev/null +++ b/pre_commit/all_languages.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from pre_commit.lang_base import Language +from pre_commit.languages import conda +from pre_commit.languages import coursier +from pre_commit.languages import dart +from pre_commit.languages import docker +from pre_commit.languages import docker_image +from pre_commit.languages import dotnet +from pre_commit.languages import fail +from pre_commit.languages import golang +from pre_commit.languages import lua +from pre_commit.languages import node +from pre_commit.languages import perl +from pre_commit.languages import pygrep +from pre_commit.languages import python +from pre_commit.languages import r +from pre_commit.languages import ruby +from pre_commit.languages import rust +from pre_commit.languages import script +from pre_commit.languages import swift +from pre_commit.languages import system + + +languages: dict[str, Language] = { + 'conda': conda, + 'coursier': coursier, + 'dart': dart, + 'docker': docker, + 'docker_image': docker_image, + 'dotnet': dotnet, + 'fail': fail, + 'golang': golang, + 'lua': lua, + 'node': node, + 'perl': perl, + 'pygrep': pygrep, + 'python': python, + 'r': r, + 'ruby': ruby, + 'rust': rust, + 'script': script, + 'swift': swift, + 'system': system, + # TODO: fully deprecate `python_venv` + 'python_venv': python, +} +language_names = sorted(languages) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index e191d3a..9ff38c6 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -12,8 +12,8 @@ import cfgv from identify.identify import ALL_TAGS import pre_commit.constants as C +from pre_commit.all_languages import language_names from pre_commit.errors import FatalError -from pre_commit.languages.all import all_languages from pre_commit.yaml import yaml_load logger = logging.getLogger('pre_commit') @@ -49,7 +49,7 @@ MANIFEST_HOOK_DICT = cfgv.Map( cfgv.Required('id', cfgv.check_string), cfgv.Required('name', cfgv.check_string), cfgv.Required('entry', cfgv.check_string), - cfgv.Required('language', cfgv.check_one_of(all_languages)), + cfgv.Required('language', cfgv.check_one_of(language_names)), cfgv.Optional('alias', cfgv.check_string, ''), cfgv.Optional('files', check_string_regex, ''), @@ -281,8 +281,8 @@ CONFIG_REPO_DICT = cfgv.Map( ) DEFAULT_LANGUAGE_VERSION = cfgv.Map( 'DefaultLanguageVersion', None, - cfgv.NoAdditionalKeys(all_languages), - *(cfgv.Optional(x, cfgv.check_string, C.DEFAULT) for x in all_languages), + cfgv.NoAdditionalKeys(language_names), + *(cfgv.Optional(x, cfgv.check_string, C.DEFAULT) for x in language_names), ) CONFIG_SCHEMA = cfgv.Map( 'Config', None, diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index 6f7af4e..842fb3a 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -42,6 +42,14 @@ def _migrate_sha_to_rev(contents: str) -> str: return re.sub(r'(\n\s+)sha:', r'\1rev:', contents) +def _migrate_python_venv(contents: str) -> str: + return re.sub( + r'(\n\s+)language: python_venv\b', + r'\1language: python', + contents, + ) + + def migrate_config(config_file: str, quiet: bool = False) -> int: with open(config_file) as f: orig_contents = contents = f.read() @@ -55,6 +63,7 @@ def migrate_config(config_file: str, quiet: bool = False) -> int: contents = _migrate_map(contents) contents = _migrate_sha_to_rev(contents) + contents = _migrate_python_venv(contents) if contents != orig_contents: with open(config_file, 'w') as f: diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index a7eb4f4..c9bc55b 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -19,9 +19,9 @@ from identify.identify import tags_from_path from pre_commit import color from pre_commit import git from pre_commit import output +from pre_commit.all_languages import languages from pre_commit.clientlib import load_config from pre_commit.hook import Hook -from pre_commit.languages.all import languages from pre_commit.repository import all_hooks from pre_commit.repository import install_hook_envs from pre_commit.staged_files_only import staged_files_only diff --git a/pre_commit/lang_base.py b/pre_commit/lang_base.py new file mode 100644 index 0000000..9480c55 --- /dev/null +++ b/pre_commit/lang_base.py @@ -0,0 +1,196 @@ +from __future__ import annotations + +import contextlib +import multiprocessing +import os +import random +import re +import shlex +from typing import Any +from typing import ContextManager +from typing import Generator +from typing import NoReturn +from typing import Protocol +from typing import Sequence + +import pre_commit.constants as C +from pre_commit import parse_shebang +from pre_commit.prefix import Prefix +from pre_commit.util import cmd_output_b +from pre_commit.xargs import xargs + +FIXED_RANDOM_SEED = 1542676187 + +SHIMS_RE = re.compile(r'[/\\]shims[/\\]') + + +class Language(Protocol): + # Use `None` for no installation / environment + @property + def ENVIRONMENT_DIR(self) -> str | None: ... + # return a value to replace `'default` for `language_version` + def get_default_version(self) -> str: ... + # return whether the environment is healthy (or should be rebuilt) + def health_check(self, prefix: Prefix, version: str) -> str | None: ... + + # install a repository for the given language and language_version + def install_environment( + self, + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], + ) -> None: + ... + + # modify the environment for hook execution + def in_env(self, prefix: Prefix, version: str) -> ContextManager[None]: ... + + # execute a hook and return the exit code and output + def run_hook( + self, + prefix: Prefix, + entry: str, + args: Sequence[str], + file_args: Sequence[str], + *, + is_local: bool, + require_serial: bool, + color: bool, + ) -> tuple[int, bytes]: + ... + + +def exe_exists(exe: str) -> bool: + found = parse_shebang.find_executable(exe) + if found is None: # exe exists + return False + + homedir = os.path.expanduser('~') + try: + common: str | None = os.path.commonpath((found, homedir)) + except ValueError: # on windows, different drives raises ValueError + common = None + + return ( + # it is not in a /shims/ directory + not SHIMS_RE.search(found) and + ( + # the homedir is / (docker, service user, etc.) + os.path.dirname(homedir) == homedir or + # the exe is not contained in the home directory + common != homedir + ) + ) + + +def setup_cmd(prefix: Prefix, cmd: tuple[str, ...], **kwargs: Any) -> None: + cmd_output_b(*cmd, cwd=prefix.prefix_dir, **kwargs) + + +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: + if version != C.DEFAULT: + raise AssertionError( + f'for now, pre-commit requires system-installed {binary} -- ' + f'you selected `language_version: {version}`', + ) + + +def assert_no_additional_deps( + lang: str, + additional_deps: Sequence[str], +) -> None: + if additional_deps: + raise AssertionError( + f'for now, pre-commit does not support ' + f'additional_dependencies for {lang} -- ' + f'you selected `additional_dependencies: {additional_deps}`', + ) + + +def basic_get_default_version() -> str: + return C.DEFAULT + + +def basic_health_check(prefix: Prefix, language_version: str) -> str | None: + return None + + +def no_install( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> NoReturn: + raise AssertionError('This language is not installable') + + +@contextlib.contextmanager +def no_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + yield + + +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. + if 'TRAVIS' in os.environ: + return 2 + else: + try: + return multiprocessing.cpu_count() + except NotImplementedError: + return 1 + + +def _shuffled(seq: Sequence[str]) -> list[str]: + """Deterministically shuffle""" + fixed_random = random.Random() + fixed_random.seed(FIXED_RANDOM_SEED, version=1) + + seq = list(seq) + fixed_random.shuffle(seq) + return seq + + +def run_xargs( + cmd: tuple[str, ...], + file_args: Sequence[str], + *, + 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]: + return run_xargs( + hook_cmd(entry, args), + file_args, + require_serial=require_serial, + color=color, + ) diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py deleted file mode 100644 index d952ae1..0000000 --- a/pre_commit/languages/all.py +++ /dev/null @@ -1,99 +0,0 @@ -from __future__ import annotations - -from typing import ContextManager -from typing import Protocol -from typing import Sequence - -from pre_commit.languages import conda -from pre_commit.languages import coursier -from pre_commit.languages import dart -from pre_commit.languages import docker -from pre_commit.languages import docker_image -from pre_commit.languages import dotnet -from pre_commit.languages import fail -from pre_commit.languages import golang -from pre_commit.languages import lua -from pre_commit.languages import node -from pre_commit.languages import perl -from pre_commit.languages import pygrep -from pre_commit.languages import python -from pre_commit.languages import r -from pre_commit.languages import ruby -from pre_commit.languages import rust -from pre_commit.languages import script -from pre_commit.languages import swift -from pre_commit.languages import system -from pre_commit.prefix import Prefix - - -class Language(Protocol): - # Use `None` for no installation / environment - @property - def ENVIRONMENT_DIR(self) -> str | None: ... - # return a value to replace `'default` for `language_version` - def get_default_version(self) -> str: ... - - # return whether the environment is healthy (or should be rebuilt) - def health_check( - self, - prefix: Prefix, - language_version: str, - ) -> str | None: - ... - - # install a repository for the given language and language_version - def install_environment( - self, - prefix: Prefix, - version: str, - additional_dependencies: Sequence[str], - ) -> None: - ... - - # modify the environment for hook execution - def in_env( - self, - prefix: Prefix, - version: str, - ) -> ContextManager[None]: - ... - - # execute a hook and return the exit code and output - def run_hook( - self, - prefix: Prefix, - entry: str, - args: Sequence[str], - file_args: Sequence[str], - *, - is_local: bool, - require_serial: bool, - color: bool, - ) -> tuple[int, bytes]: - ... - - -languages: dict[str, Language] = { - 'conda': conda, - 'coursier': coursier, - 'dart': dart, - 'docker': docker, - 'docker_image': docker_image, - 'dotnet': dotnet, - 'fail': fail, - 'golang': golang, - 'lua': lua, - 'node': node, - 'perl': perl, - 'pygrep': pygrep, - 'python': python, - 'r': r, - 'ruby': ruby, - 'rust': rust, - 'script': script, - 'swift': swift, - 'system': system, - # TODO: fully deprecate `python_venv` - 'python_venv': python, -} -all_languages = sorted(languages) diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index e2fb019..05f1d29 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -5,19 +5,19 @@ import os from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import SubstitutionT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'conda' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def get_env_patch(env: str) -> PatchesT: @@ -41,7 +41,7 @@ def get_env_patch(env: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -60,11 +60,11 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - helpers.assert_version_default('conda', version) + lang_base.assert_version_default('conda', version) conda_exe = _conda_exe() - env_dir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + env_dir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) cmd_output_b( conda_exe, 'env', 'create', '-p', env_dir, '--file', 'environment.yml', cwd=prefix.prefix_dir, diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index 6075758..9c5fbfe 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -5,19 +5,19 @@ import os.path from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var from pre_commit.errors import FatalError -from pre_commit.languages import helpers from pre_commit.parse_shebang import find_executable from pre_commit.prefix import Prefix ENVIRONMENT_DIR = 'coursier' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def install_environment( @@ -25,7 +25,7 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - helpers.assert_version_default('coursier', version) + lang_base.assert_version_default('coursier', version) # Support both possible executable names (either "cs" or "coursier") cs = find_executable('cs') or find_executable('coursier') @@ -35,12 +35,12 @@ def install_environment( 'executables in the application search path', ) - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) def _install(*opts: str) -> None: assert cs is not None - helpers.run_setup_cmd(prefix, (cs, 'fetch', *opts)) - helpers.run_setup_cmd(prefix, (cs, 'install', '--dir', envdir, *opts)) + lang_base.setup_cmd(prefix, (cs, 'fetch', *opts)) + lang_base.setup_cmd(prefix, (cs, 'install', '--dir', envdir, *opts)) with in_env(prefix, version): channel = prefix.path('.pre-commit-channel') @@ -71,6 +71,6 @@ def get_env_patch(target_dir: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py index e3c1c58..e8539ca 100644 --- a/pre_commit/languages/dart.py +++ b/pre_commit/languages/dart.py @@ -7,19 +7,19 @@ import tempfile from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import win_exe from pre_commit.yaml import yaml_load ENVIRONMENT_DIR = 'dartenv' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def get_env_patch(venv: str) -> PatchesT: @@ -30,7 +30,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -40,9 +40,9 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - helpers.assert_version_default('dart', version) + lang_base.assert_version_default('dart', version) - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) bin_dir = os.path.join(envdir, 'bin') def _install_dir(prefix_p: Prefix, pub_cache: str) -> None: @@ -51,10 +51,10 @@ def install_environment( with open(prefix_p.path('pubspec.yaml')) as f: pubspec_contents = yaml_load(f) - helpers.run_setup_cmd(prefix_p, ('dart', 'pub', 'get'), env=dart_env) + lang_base.setup_cmd(prefix_p, ('dart', 'pub', 'get'), env=dart_env) for executable in pubspec_contents['executables']: - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix_p, ( 'dart', 'compile', 'exe', @@ -77,7 +77,7 @@ def install_environment( else: dep_cmd = (dep,) - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix, ('dart', 'pub', 'cache', 'add', *dep_cmd), env={**os.environ, 'PUB_CACHE': dep_tmp}, diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index e80c959..8e53ca9 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -5,16 +5,16 @@ import json import os from typing import Sequence -from pre_commit.languages import helpers +from pre_commit import lang_base from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'docker' PRE_COMMIT_LABEL = 'PRE_COMMIT' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -in_env = helpers.no_env # no special environment for docker +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +in_env = lang_base.no_env # no special environment for docker def _is_in_docker() -> bool: @@ -84,16 +84,16 @@ def build_docker_image( cmd += ('--pull',) # This must come last for old versions of docker. See #477 cmd += ('.',) - helpers.run_setup_cmd(prefix, cmd) + lang_base.setup_cmd(prefix, cmd) def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: # pragma: win32 no cover - helpers.assert_version_default('docker', version) - helpers.assert_no_additional_deps('docker', additional_dependencies) + lang_base.assert_version_default('docker', version) + lang_base.assert_no_additional_deps('docker', additional_dependencies) - directory = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + directory = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) # Docker doesn't really have relevant disk environment, but pre-commit # still needs to cleanup its state files on failure @@ -135,12 +135,11 @@ def run_hook( # automated cleanup of docker images. build_docker_image(prefix, pull=False) - entry_exe, *cmd_rest = helpers.hook_cmd(entry, args) + entry_exe, *cmd_rest = lang_base.hook_cmd(entry, args) entry_tag = ('--entrypoint', entry_exe, docker_tag(prefix)) - cmd = (*docker_cmd(), *entry_tag, *cmd_rest) - return helpers.run_xargs( - cmd, + return lang_base.run_xargs( + (*docker_cmd(), *entry_tag, *cmd_rest), file_args, require_serial=require_serial, color=color, diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index 8e5f2c0..26f006e 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -2,15 +2,15 @@ from __future__ import annotations from typing import Sequence -from pre_commit.languages import helpers +from pre_commit import lang_base from pre_commit.languages.docker import docker_cmd from pre_commit.prefix import Prefix ENVIRONMENT_DIR = None -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -install_environment = helpers.no_install -in_env = helpers.no_env +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +install_environment = lang_base.no_install +in_env = lang_base.no_env def run_hook( @@ -23,8 +23,8 @@ def run_hook( require_serial: bool, color: bool, ) -> tuple[int, bytes]: # pragma: win32 no cover - cmd = docker_cmd() + helpers.hook_cmd(entry, args) - return helpers.run_xargs( + cmd = docker_cmd() + lang_base.hook_cmd(entry, args) + return lang_base.run_xargs( cmd, file_args, require_serial=require_serial, diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index 4c3955e..e9568f2 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -9,18 +9,18 @@ import zipfile from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix ENVIRONMENT_DIR = 'dotnetenv' BIN_DIR = 'bin' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def get_env_patch(venv: str) -> PatchesT: @@ -31,7 +31,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -57,19 +57,19 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - helpers.assert_version_default('dotnet', version) - helpers.assert_no_additional_deps('dotnet', additional_dependencies) + lang_base.assert_version_default('dotnet', version) + lang_base.assert_no_additional_deps('dotnet', additional_dependencies) - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) - build_dir = 'pre-commit-build' + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) + build_dir = prefix.path('pre-commit-build') # Build & pack nupkg file - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix, ( 'dotnet', 'pack', '--configuration', 'Release', - '--output', build_dir, + '--property', f'PackageOutputPath={build_dir}', ), ) @@ -99,7 +99,7 @@ def install_environment( # Install to bin dir with _nuget_config_no_sources() as nuget_config: - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix, ( 'dotnet', 'tool', 'install', @@ -109,7 +109,3 @@ def install_environment( tool_id, ), ) - - # Clean the git dir, ignoring the environment dir - clean_cmd = ('git', 'clean', '-ffxd', '-e', f'{ENVIRONMENT_DIR}-*') - helpers.run_setup_cmd(prefix, clean_cmd) diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index 33df067..a8ec6a5 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -2,14 +2,14 @@ from __future__ import annotations from typing import Sequence -from pre_commit.languages import helpers +from pre_commit import lang_base from pre_commit.prefix import Prefix ENVIRONMENT_DIR = None -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -install_environment = helpers.no_install -in_env = helpers.no_env +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +install_environment = lang_base.no_install +in_env = lang_base.no_env def run_hook( diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 3c4b652..bea91e9 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -19,17 +19,17 @@ from typing import Protocol from typing import Sequence import pre_commit.constants as C +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output from pre_commit.util import rmtree ENVIRONMENT_DIR = 'golangenv' -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook _ARCH_ALIASES = { 'x86_64': 'amd64', @@ -60,7 +60,7 @@ else: # pragma: win32 no cover @functools.lru_cache(maxsize=1) def get_default_version() -> str: - if helpers.exe_exists('go'): + if lang_base.exe_exists('go'): return 'system' else: return C.DEFAULT @@ -121,7 +121,7 @@ def _install_go(version: str, dest: str) -> None: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir, version)): yield @@ -131,7 +131,7 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - env_dir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + env_dir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) if version != 'system': _install_go(version, env_dir) @@ -149,9 +149,9 @@ def install_environment( os.path.join(env_dir, '.go', 'bin'), os.environ['PATH'], )) - helpers.run_setup_cmd(prefix, ('go', 'install', './...'), env=env) + lang_base.setup_cmd(prefix, ('go', 'install', './...'), env=env) for dependency in additional_dependencies: - helpers.run_setup_cmd(prefix, ('go', 'install', dependency), env=env) + lang_base.setup_cmd(prefix, ('go', 'install', dependency), env=env) # save some disk space -- we don't need this after installation pkgdir = os.path.join(env_dir, 'pkg') diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py deleted file mode 100644 index d1be409..0000000 --- a/pre_commit/languages/helpers.py +++ /dev/null @@ -1,158 +0,0 @@ -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 Sequence - -import pre_commit.constants as C -from pre_commit import parse_shebang -from pre_commit.prefix import Prefix -from pre_commit.util import cmd_output_b -from pre_commit.xargs import xargs - -FIXED_RANDOM_SEED = 1542676187 - -SHIMS_RE = re.compile(r'[/\\]shims[/\\]') - - -def exe_exists(exe: str) -> bool: - found = parse_shebang.find_executable(exe) - if found is None: # exe exists - return False - - homedir = os.path.expanduser('~') - try: - common: str | None = os.path.commonpath((found, homedir)) - except ValueError: # on windows, different drives raises ValueError - common = None - - return ( - # it is not in a /shims/ directory - not SHIMS_RE.search(found) and - ( - # the homedir is / (docker, service user, etc.) - os.path.dirname(homedir) == homedir or - # the exe is not contained in the home directory - common != homedir - ) - ) - - -def run_setup_cmd(prefix: Prefix, cmd: tuple[str, ...], **kwargs: Any) -> None: - cmd_output_b(*cmd, cwd=prefix.prefix_dir, **kwargs) - - -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: - if version != C.DEFAULT: - raise AssertionError( - f'for now, pre-commit requires system-installed {binary} -- ' - f'you selected `language_version: {version}`', - ) - - -def assert_no_additional_deps( - lang: str, - additional_deps: Sequence[str], -) -> None: - if additional_deps: - raise AssertionError( - f'for now, pre-commit does not support ' - f'additional_dependencies for {lang} -- ' - f'you selected `additional_dependencies: {additional_deps}`', - ) - - -def basic_get_default_version() -> str: - return C.DEFAULT - - -def basic_health_check(prefix: Prefix, language_version: str) -> str | None: - return None - - -def no_install( - prefix: Prefix, - version: str, - additional_dependencies: Sequence[str], -) -> NoReturn: - raise AssertionError('This language is not installable') - - -@contextlib.contextmanager -def no_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - yield - - -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. - if 'TRAVIS' in os.environ: - return 2 - else: - try: - return multiprocessing.cpu_count() - except NotImplementedError: - return 1 - - -def _shuffled(seq: Sequence[str]) -> list[str]: - """Deterministically shuffle""" - fixed_random = random.Random() - fixed_random.seed(FIXED_RANDOM_SEED, version=1) - - seq = list(seq) - fixed_random.shuffle(seq) - return seq - - -def run_xargs( - cmd: tuple[str, ...], - file_args: Sequence[str], - *, - 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]: - 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 ffc40b5..12d0661 100644 --- a/pre_commit/languages/lua.py +++ b/pre_commit/languages/lua.py @@ -6,17 +6,17 @@ import sys from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output ENVIRONMENT_DIR = 'lua_env' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def _get_lua_version() -> str: # pragma: win32 no cover @@ -45,7 +45,7 @@ def get_env_patch(d: str) -> PatchesT: # pragma: win32 no cover @contextlib.contextmanager # pragma: win32 no cover def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -55,9 +55,9 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: # pragma: win32 no cover - helpers.assert_version_default('lua', version) + lang_base.assert_version_default('lua', version) - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with in_env(prefix, version): # luarocks doesn't bootstrap a tree prior to installing # so ensure the directory exists. @@ -66,10 +66,10 @@ def install_environment( # Older luarocks (e.g., 2.4.2) expect the rockspec as an arg for rockspec in prefix.star('.rockspec'): make_cmd = ('luarocks', '--tree', envdir, 'make', rockspec) - helpers.run_setup_cmd(prefix, make_cmd) + lang_base.setup_cmd(prefix, make_cmd) # luarocks can't install multiple packages at once # so install them individually. for dependency in additional_dependencies: cmd = ('luarocks', '--tree', envdir, 'install', dependency) - helpers.run_setup_cmd(prefix, cmd) + lang_base.setup_cmd(prefix, cmd) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 9688da3..66d6136 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -8,11 +8,11 @@ from typing import Generator from typing import Sequence import pre_commit.constants as C +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.languages.python import bin_dir from pre_commit.prefix import Prefix from pre_commit.util import cmd_output @@ -20,7 +20,7 @@ from pre_commit.util import cmd_output_b from pre_commit.util import rmtree ENVIRONMENT_DIR = 'node_env' -run_hook = helpers.basic_run_hook +run_hook = lang_base.basic_run_hook @functools.lru_cache(maxsize=1) @@ -30,7 +30,7 @@ def get_default_version() -> str: return C.DEFAULT # if node is already installed, we can save a bunch of setup time by # using the installed version - elif all(helpers.exe_exists(exe) for exe in ('node', 'npm')): + elif all(lang_base.exe_exists(exe) for exe in ('node', 'npm')): return 'system' else: return C.DEFAULT @@ -60,13 +60,13 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield -def health_check(prefix: Prefix, language_version: str) -> str | None: - with in_env(prefix, language_version): +def health_check(prefix: Prefix, version: str) -> str | None: + with in_env(prefix, version): retcode, _, _ = cmd_output_b('node', '--version', check=False) if retcode != 0: # pragma: win32 no cover return f'`node --version` returned {retcode}' @@ -78,7 +78,7 @@ def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: assert prefix.exists('package.json') - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx?f=255&MSPPError=-2147217396#maxpath if sys.platform == 'win32': # pragma: no cover @@ -96,13 +96,13 @@ def install_environment( 'npm', 'install', '--dev', '--prod', '--ignore-prepublish', '--no-progress', '--no-save', ) - helpers.run_setup_cmd(prefix, local_install_cmd) + lang_base.setup_cmd(prefix, local_install_cmd) _, pkg, _ = cmd_output('npm', 'pack', cwd=prefix.prefix_dir) pkg = prefix.path(pkg.strip()) install = ('npm', 'install', '-g', pkg, *additional_dependencies) - helpers.run_setup_cmd(prefix, install) + lang_base.setup_cmd(prefix, install) # clean these up after installation if prefix.exists('node_modules'): # pragma: win32 no cover diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py index 2530c0e..2a7f162 100644 --- a/pre_commit/languages/perl.py +++ b/pre_commit/languages/perl.py @@ -6,16 +6,16 @@ import shlex from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix ENVIRONMENT_DIR = 'perl_env' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def get_env_patch(venv: str) -> PatchesT: @@ -34,7 +34,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -42,9 +42,9 @@ def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: - helpers.assert_version_default('perl', version) + lang_base.assert_version_default('perl', version) with in_env(prefix, version): - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix, ('cpan', '-T', '.', *additional_dependencies), ) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index f0eb9a9..ec55560 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -7,16 +7,16 @@ from typing import NamedTuple from typing import Pattern from typing import Sequence +from pre_commit import lang_base from pre_commit import output -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.xargs import xargs ENVIRONMENT_DIR = None -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -install_environment = helpers.no_install -in_env = helpers.no_env +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +install_environment = lang_base.no_install +in_env = lang_base.no_env def _process_filename_by_line(pattern: Pattern[bytes], filename: str) -> int: diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index c373646..976674e 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -8,11 +8,11 @@ from typing import Generator from typing import Sequence import pre_commit.constants as C +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.parse_shebang import find_executable from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError @@ -21,7 +21,7 @@ from pre_commit.util import cmd_output_b from pre_commit.util import win_exe ENVIRONMENT_DIR = 'py_env' -run_hook = helpers.basic_run_hook +run_hook = lang_base.basic_run_hook @functools.lru_cache(maxsize=None) @@ -153,13 +153,13 @@ def norm_version(version: str) -> str | None: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield -def health_check(prefix: Prefix, language_version: str) -> str | None: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) +def health_check(prefix: Prefix, version: str) -> str | None: + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) pyvenv_cfg = os.path.join(envdir, 'pyvenv.cfg') # created with "old" virtualenv @@ -202,7 +202,7 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) venv_cmd = [sys.executable, '-mvirtualenv', envdir] python = norm_version(version) if python is not None: @@ -211,4 +211,4 @@ def install_environment( cmd_output_b(*venv_cmd, cwd='/') with in_env(prefix, version): - helpers.run_setup_cmd(prefix, install_cmd) + lang_base.setup_cmd(prefix, install_cmd) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index e238365..138a26e 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -7,18 +7,18 @@ import shutil from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b from pre_commit.util import win_exe ENVIRONMENT_DIR = 'renv' RSCRIPT_OPTS = ('--no-save', '--no-restore', '--no-site-file', '--no-environ') -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check def get_env_patch(venv: str) -> PatchesT: @@ -30,7 +30,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -93,7 +93,7 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - env_dir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + env_dir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) os.makedirs(env_dir, exist_ok=True) shutil.copy(prefix.path('renv.lock'), env_dir) shutil.copytree(prefix.path('renv'), os.path.join(env_dir, 'renv')) @@ -166,7 +166,7 @@ def run_hook( color: bool, ) -> tuple[int, bytes]: cmd = _cmd_from_hook(prefix, entry, args, is_local=is_local) - return helpers.run_xargs( + return lang_base.run_xargs( cmd, file_args, require_serial=require_serial, diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 4416f72..76631f2 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -2,30 +2,35 @@ from __future__ import annotations import contextlib import functools +import importlib.resources import os.path import shutil import tarfile from typing import Generator +from typing import IO from typing import Sequence import pre_commit.constants as C +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError -from pre_commit.util import resource_bytesio ENVIRONMENT_DIR = 'rbenv' -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook + + +def _resource_bytesio(filename: str) -> IO[bytes]: + return importlib.resources.open_binary('pre_commit.resources', filename) @functools.lru_cache(maxsize=1) def get_default_version() -> str: - if all(helpers.exe_exists(exe) for exe in ('ruby', 'gem')): + if all(lang_base.exe_exists(exe) for exe in ('ruby', 'gem')): return 'system' else: return C.DEFAULT @@ -68,13 +73,13 @@ def get_env_patch( @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir, version)): yield def _extract_resource(filename: str, dest: str) -> None: - with resource_bytesio(filename) as bio: + with _resource_bytesio(filename) as bio: with tarfile.open(fileobj=bio) as tf: tf.extractall(dest) @@ -83,7 +88,7 @@ def _install_rbenv( prefix: Prefix, version: str, ) -> None: # pragma: win32 no cover - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) _extract_resource('rbenv.tar.gz', prefix.path('.')) shutil.move(prefix.path('rbenv'), envdir) @@ -100,10 +105,10 @@ def _install_ruby( version: str, ) -> None: # pragma: win32 no cover try: - helpers.run_setup_cmd(prefix, ('rbenv', 'download', version)) + lang_base.setup_cmd(prefix, ('rbenv', 'download', version)) except CalledProcessError: # pragma: no cover (usually find with download) # Failed to download from mirror for some reason, build it instead - helpers.run_setup_cmd(prefix, ('rbenv', 'install', version)) + lang_base.setup_cmd(prefix, ('rbenv', 'install', version)) def install_environment( @@ -114,17 +119,17 @@ def install_environment( with in_env(prefix, version): # Need to call this before installing so rbenv's directories # are set up - helpers.run_setup_cmd(prefix, ('rbenv', 'init', '-')) + lang_base.setup_cmd(prefix, ('rbenv', 'init', '-')) if version != C.DEFAULT: _install_ruby(prefix, version) # Need to call this after installing to set up the shims - helpers.run_setup_cmd(prefix, ('rbenv', 'rehash')) + lang_base.setup_cmd(prefix, ('rbenv', 'rehash')) with in_env(prefix, version): - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix, ('gem', 'build', *prefix.star('.gemspec')), ) - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix, ( 'gem', 'install', diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 391fd86..e98e0d0 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -11,19 +11,19 @@ from typing import Generator from typing import Sequence import pre_commit.constants as C +from pre_commit import lang_base from pre_commit import parse_shebang from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b from pre_commit.util import make_executable from pre_commit.util import win_exe ENVIRONMENT_DIR = 'rustenv' -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook @functools.lru_cache(maxsize=1) @@ -63,7 +63,7 @@ def get_env_patch(target_dir: str, version: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir, version)): yield @@ -78,7 +78,7 @@ def _add_dependencies( crate = f'{name}@{spec or "*"}' crates.append(crate) - helpers.run_setup_cmd(prefix, ('cargo', 'add', *crates)) + lang_base.setup_cmd(prefix, ('cargo', 'add', *crates)) def install_rust_with_toolchain(toolchain: str) -> None: @@ -116,7 +116,7 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) # There are two cases where we might want to specify more dependencies: # as dependencies for the library being built, and as binary packages diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 08325f4..89a3ab2 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -2,14 +2,14 @@ from __future__ import annotations from typing import Sequence -from pre_commit.languages import helpers +from pre_commit import lang_base from pre_commit.prefix import Prefix ENVIRONMENT_DIR = None -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -install_environment = helpers.no_install -in_env = helpers.no_env +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +install_environment = lang_base.no_install +in_env = lang_base.no_env def run_hook( @@ -22,9 +22,9 @@ def run_hook( require_serial: bool, color: bool, ) -> tuple[int, bytes]: - cmd = helpers.hook_cmd(entry, args) + cmd = lang_base.hook_cmd(entry, args) cmd = (prefix.path(cmd[0]), *cmd[1:]) - return helpers.run_xargs( + return lang_base.run_xargs( cmd, file_args, require_serial=require_serial, diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index c66ad5f..8250ab7 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -5,10 +5,10 @@ import os from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b @@ -16,9 +16,9 @@ BUILD_DIR = '.build' BUILD_CONFIG = 'release' ENVIRONMENT_DIR = 'swift_env' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def get_env_patch(venv: str) -> PatchesT: # pragma: win32 no cover @@ -28,7 +28,7 @@ def get_env_patch(venv: str) -> PatchesT: # pragma: win32 no cover @contextlib.contextmanager # pragma: win32 no cover def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -36,9 +36,9 @@ def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: # pragma: win32 no cover - helpers.assert_version_default('swift', version) - helpers.assert_no_additional_deps('swift', additional_dependencies) - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + lang_base.assert_version_default('swift', version) + lang_base.assert_no_additional_deps('swift', additional_dependencies) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) # Build the swift package os.mkdir(envdir) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 204cad7..f6ad688 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -1,10 +1,10 @@ from __future__ import annotations -from pre_commit.languages import helpers +from pre_commit import lang_base ENVIRONMENT_DIR = None -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -install_environment = helpers.no_install -in_env = helpers.no_env -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +install_environment = lang_base.no_install +in_env = lang_base.no_env +run_hook = lang_base.basic_run_hook diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 616faf5..040f238 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -3,17 +3,18 @@ from __future__ import annotations import json import logging import os +import shlex from typing import Any from typing import Sequence import pre_commit.constants as C +from pre_commit.all_languages import languages from pre_commit.clientlib import load_manifest from pre_commit.clientlib import LOCAL from pre_commit.clientlib import META from pre_commit.clientlib import parse_version from pre_commit.hook import Hook -from pre_commit.languages.all import languages -from pre_commit.languages.helpers import environment_dir +from pre_commit.lang_base import environment_dir from pre_commit.prefix import Prefix from pre_commit.store import Store from pre_commit.util import clean_path_on_failure @@ -32,7 +33,7 @@ def _state_filename_v2(venv: str) -> str: def _state(additional_deps: Sequence[str]) -> object: - return {'additional_dependencies': sorted(additional_deps)} + return {'additional_dependencies': additional_deps} def _read_state(venv: str) -> object | None: @@ -68,6 +69,14 @@ def _hook_install(hook: Hook) -> None: logger.info('Once installed this environment will be reused.') logger.info('This may take a few minutes...') + if hook.language == 'python_venv': + logger.warning( + f'`repo: {hook.src}` uses deprecated `language: python_venv`. ' + f'This is an alias for `language: python`. ' + f'Often `pre-commit autoupdate --repo {shlex.quote(hook.src)}` ' + f'will fix this.', + ) + lang = languages[hook.language] assert lang.ENVIRONMENT_DIR is not None diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 172fb20..8812356 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -7,6 +7,7 @@ import time from typing import Generator from pre_commit import git +from pre_commit.errors import FatalError from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b @@ -49,12 +50,16 @@ def _intent_to_add_cleared() -> Generator[None, None, None]: @contextlib.contextmanager def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: tree = cmd_output('git', 'write-tree')[1].strip() - retcode, diff_stdout_binary, _ = cmd_output_b( + diff_cmd = ( 'git', 'diff-index', '--ignore-submodules', '--binary', '--exit-code', '--no-color', '--no-ext-diff', tree, '--', - check=False, ) - if retcode and diff_stdout_binary.strip(): + retcode, diff_stdout, diff_stderr = cmd_output_b(*diff_cmd, check=False) + if retcode == 0: + # There weren't any staged files so we don't need to do anything + # special + yield + elif retcode == 1 and diff_stdout.strip(): patch_filename = f'patch{int(time.time())}-{os.getpid()}' patch_filename = os.path.join(patch_dir, patch_filename) logger.warning('Unstaged files detected.') @@ -62,7 +67,7 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: # Save the current unstaged changes as a patch os.makedirs(patch_dir, exist_ok=True) with open(patch_filename, 'wb') as patch_file: - patch_file.write(diff_stdout_binary) + patch_file.write(diff_stdout) # prevent recursive post-checkout hooks (#1418) no_checkout_env = dict(os.environ, _PRE_COMMIT_SKIP_POST_CHECKOUT='1') @@ -86,10 +91,12 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: _git_apply(patch_filename) logger.info(f'Restored changes from {patch_filename}.') - else: - # There weren't any staged files so we don't need to do anything - # special - yield + else: # pragma: win32 no cover + # some error occurred while requesting the diff + e = CalledProcessError(retcode, diff_cmd, b'', diff_stderr) + raise FatalError( + f'pre-commit failed to diff -- perhaps due to permissions?\n\n{e}', + ) @contextlib.contextmanager diff --git a/pre_commit/store.py b/pre_commit/store.py index 6ddc7c4..487e3e7 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -125,7 +125,7 @@ class Store: @classmethod def db_repo_name(cls, repo: str, deps: Sequence[str]) -> str: if deps: - return f'{repo}:{",".join(sorted(deps))}' + return f'{repo}:{",".join(deps)}' else: return repo diff --git a/pre_commit/util.py b/pre_commit/util.py index 8ea4844..ea0d4f5 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -12,7 +12,6 @@ from types import TracebackType from typing import Any from typing import Callable from typing import Generator -from typing import IO from pre_commit import parse_shebang @@ -36,10 +35,6 @@ def clean_path_on_failure(path: str) -> Generator[None, None, None]: raise -def resource_bytesio(filename: str) -> IO[bytes]: - return importlib.resources.open_binary('pre_commit.resources', filename) - - def resource_text(filename: str) -> str: return importlib.resources.read_text('pre_commit.resources', filename) @@ -67,7 +62,7 @@ class CalledProcessError(RuntimeError): def __bytes__(self) -> bytes: def _indent_or_none(part: bytes | None) -> bytes: if part: - return b'\n ' + part.replace(b'\n', b'\n ') + return b'\n ' + part.replace(b'\n', b'\n ').rstrip() else: return b' (none)' diff --git a/setup.cfg b/setup.cfg index 56b856c..d1f649f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.0.4 +version = 3.1.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown diff --git a/testing/language_helpers.py b/testing/language_helpers.py index b9c5384..ead8dae 100644 --- a/testing/language_helpers.py +++ b/testing/language_helpers.py @@ -3,7 +3,7 @@ from __future__ import annotations import os from typing import Sequence -from pre_commit.languages.all import Language +from pre_commit.lang_base import Language from pre_commit.prefix import Prefix @@ -16,13 +16,16 @@ def run_language( version: str | None = None, deps: Sequence[str] = (), is_local: bool = False, + require_serial: bool = True, + color: bool = False, ) -> tuple[int, bytes]: prefix = Prefix(str(path)) version = version or language.get_default_version() - language.install_environment(prefix, version, deps) - health_error = language.health_check(prefix, version) - assert health_error is None, health_error + if language.ENVIRONMENT_DIR is not None: + language.install_environment(prefix, version, deps) + health_error = language.health_check(prefix, version) + assert health_error is None, health_error with language.in_env(prefix, version): ret, out = language.run_hook( prefix, @@ -30,8 +33,8 @@ def run_language( args, file_args, is_local=is_local, - require_serial=True, - color=False, + require_serial=require_serial, + color=color, ) out = out.replace(b'\r\n', b'\n') return ret, out diff --git a/testing/languages b/testing/languages new file mode 100755 index 0000000..5e8fc9e --- /dev/null +++ b/testing/languages @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import concurrent.futures +import json +import os.path +import subprocess +import sys + +EXCLUDED = frozenset(( + ('windows-latest', 'docker'), + ('windows-latest', 'docker_image'), + ('windows-latest', 'lua'), + ('windows-latest', 'swift'), +)) + + +def _lang_files(lang: str) -> frozenset[str]: + prog = f'''\ +import json +import os.path +import sys + +import pre_commit.languages.{lang} +import tests.languages.{lang}_test + +modules = sorted( + os.path.relpath(v.__file__) + for k, v in sys.modules.items() + if k.startswith(('pre_commit.', 'tests.', 'testing.')) +) +print(json.dumps(modules)) +''' + out = json.loads(subprocess.check_output((sys.executable, '-c', prog))) + return frozenset(out) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument('--all', action='store_true') + args = parser.parse_args() + + langs = [ + os.path.splitext(fname)[0] + for fname in sorted(os.listdir('pre_commit/languages')) + if fname.endswith('.py') and fname != '__init__.py' + ] + + if not args.all: + with concurrent.futures.ThreadPoolExecutor(os.cpu_count()) as exe: + by_lang = { + lang: files + for lang, files in zip(langs, exe.map(_lang_files, langs)) + } + + diff_cmd = ('git', 'diff', '--name-only', 'origin/main...HEAD') + files = set(subprocess.check_output(diff_cmd).decode().splitlines()) + + langs = [ + lang + for lang, lang_files in by_lang.items() + if lang_files & files + ] + + matched = [ + {'os': os, 'language': lang} + for os in ('windows-latest', 'ubuntu-latest') + for lang in langs + if (os, lang) not in EXCLUDED + ] + + with open(os.environ['GITHUB_OUTPUT'], 'a') as f: + f.write(f'languages={json.dumps(matched)}\n') + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/testing/resources/docker_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/docker_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 5295739..0000000 --- a/testing/resources/docker_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,17 +0,0 @@ -- id: docker-hook - name: Docker test hook - entry: echo - language: docker - files: \.txt$ - -- id: docker-hook-arg - name: Docker test hook - entry: echo -n - language: docker - files: \.txt$ - -- id: docker-hook-failing - name: Docker test hook with nonzero exit code - entry: bork - language: docker - files: \.txt$ diff --git a/testing/resources/docker_hooks_repo/Dockerfile b/testing/resources/docker_hooks_repo/Dockerfile deleted file mode 100644 index 0bd1de0..0000000 --- a/testing/resources/docker_hooks_repo/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM ubuntu:focal - -CMD ["echo", "This is overwritten by the .pre-commit-hooks.yaml 'entry'"] diff --git a/testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index e9fb245..0000000 --- a/testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,8 +0,0 @@ -- id: echo-entrypoint - name: echo (via --entrypoint) - language: docker_image - entry: --entrypoint echo ubuntu:focal -- id: echo-cmd - name: echo (via cmd) - language: docker_image - entry: ubuntu:focal echo diff --git a/testing/resources/dotnet_hooks_combo_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_combo_repo/.pre-commit-hooks.yaml deleted file mode 100644 index f221854..0000000 --- a/testing/resources/dotnet_hooks_combo_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,12 +0,0 @@ -- id: dotnet-example-hook - name: Test Project 1 - description: Test Project 1 - entry: proj1 - language: dotnet - stages: [commit] -- id: proj2 - name: Test Project 2 - description: Test Project 2 - entry: proj2 - language: dotnet - stages: [commit] diff --git a/testing/resources/dotnet_hooks_combo_repo/dotnet_hooks_combo_repo.sln b/testing/resources/dotnet_hooks_combo_repo/dotnet_hooks_combo_repo.sln deleted file mode 100644 index edb0fcb..0000000 --- a/testing/resources/dotnet_hooks_combo_repo/dotnet_hooks_combo_repo.sln +++ /dev/null @@ -1,28 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30114.105 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "proj1", "proj1\proj1.csproj", "{38A939C3-DEA4-47D7-9B75-0418C4249662}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "proj2", "proj2\proj2.csproj", "{4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {38A939C3-DEA4-47D7-9B75-0418C4249662}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {38A939C3-DEA4-47D7-9B75-0418C4249662}.Debug|Any CPU.Build.0 = Debug|Any CPU - {38A939C3-DEA4-47D7-9B75-0418C4249662}.Release|Any CPU.ActiveCfg = Release|Any CPU - {38A939C3-DEA4-47D7-9B75-0418C4249662}.Release|Any CPU.Build.0 = Release|Any CPU - {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal diff --git a/testing/resources/dotnet_hooks_combo_repo/proj1/Program.cs b/testing/resources/dotnet_hooks_combo_repo/proj1/Program.cs deleted file mode 100644 index 03876f5..0000000 --- a/testing/resources/dotnet_hooks_combo_repo/proj1/Program.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace proj1 -{ - class Program - { - static void Main(string[] args) - { - Console.Write("Hello from dotnet!\n"); - } - } -} diff --git a/testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj b/testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj deleted file mode 100644 index 861ced6..0000000 --- a/testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - Exe - net6 - - true - proj1 - ./nupkg - - - diff --git a/testing/resources/dotnet_hooks_combo_repo/proj2/Program.cs b/testing/resources/dotnet_hooks_combo_repo/proj2/Program.cs deleted file mode 100644 index 47a99a3..0000000 --- a/testing/resources/dotnet_hooks_combo_repo/proj2/Program.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace proj2 -{ - class Program - { - static void Main(string[] args) - { - Console.WriteLine("Hello World!"); - } - } -} diff --git a/testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj b/testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj deleted file mode 100644 index dfce2ca..0000000 --- a/testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - Exe - net6 - - true - proj2 - ./nupkg - - - diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/.gitignore b/testing/resources/dotnet_hooks_csproj_prefix_repo/.gitignore deleted file mode 100644 index edcd28f..0000000 --- a/testing/resources/dotnet_hooks_csproj_prefix_repo/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -bin/ -obj/ -nupkg/ diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_csproj_prefix_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 6626627..0000000 --- a/testing/resources/dotnet_hooks_csproj_prefix_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: dotnet-example-hook - name: dotnet example hook - entry: testeroni.tool - language: dotnet - files: '' diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/Program.cs b/testing/resources/dotnet_hooks_csproj_prefix_repo/Program.cs deleted file mode 100644 index 1456e8e..0000000 --- a/testing/resources/dotnet_hooks_csproj_prefix_repo/Program.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace dotnet_hooks_repo -{ - class Program - { - static void Main(string[] args) - { - Console.WriteLine("Hello from dotnet!"); - } - } -} diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/dotnet_hooks_csproj_prefix_repo.csproj b/testing/resources/dotnet_hooks_csproj_prefix_repo/dotnet_hooks_csproj_prefix_repo.csproj deleted file mode 100644 index 754b760..0000000 --- a/testing/resources/dotnet_hooks_csproj_prefix_repo/dotnet_hooks_csproj_prefix_repo.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - Exe - net7.0 - true - testeroni.tool - ./nupkg - - diff --git a/testing/resources/dotnet_hooks_csproj_repo/.gitignore b/testing/resources/dotnet_hooks_csproj_repo/.gitignore deleted file mode 100644 index edcd28f..0000000 --- a/testing/resources/dotnet_hooks_csproj_repo/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -bin/ -obj/ -nupkg/ diff --git a/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 0f514c1..0000000 --- a/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: dotnet-example-hook - name: dotnet example hook - entry: testeroni - language: dotnet - files: '' diff --git a/testing/resources/dotnet_hooks_csproj_repo/Program.cs b/testing/resources/dotnet_hooks_csproj_repo/Program.cs deleted file mode 100644 index 1456e8e..0000000 --- a/testing/resources/dotnet_hooks_csproj_repo/Program.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace dotnet_hooks_repo -{ - class Program - { - static void Main(string[] args) - { - Console.WriteLine("Hello from dotnet!"); - } - } -} diff --git a/testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj b/testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj deleted file mode 100644 index fa9879b..0000000 --- a/testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - Exe - net6 - true - testeroni - ./nupkg - - diff --git a/testing/resources/dotnet_hooks_sln_repo/.gitignore b/testing/resources/dotnet_hooks_sln_repo/.gitignore deleted file mode 100644 index edcd28f..0000000 --- a/testing/resources/dotnet_hooks_sln_repo/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -bin/ -obj/ -nupkg/ diff --git a/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 0f514c1..0000000 --- a/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: dotnet-example-hook - name: dotnet example hook - entry: testeroni - language: dotnet - files: '' diff --git a/testing/resources/dotnet_hooks_sln_repo/Program.cs b/testing/resources/dotnet_hooks_sln_repo/Program.cs deleted file mode 100644 index 04ad4e0..0000000 --- a/testing/resources/dotnet_hooks_sln_repo/Program.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace dotnet_hooks_sln_repo -{ - class Program - { - static void Main(string[] args) - { - Console.WriteLine("Hello from dotnet!"); - } - } -} diff --git a/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj b/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj deleted file mode 100644 index a4e2d00..0000000 --- a/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - Exe - net6 - true - testeroni - ./nupkg - - diff --git a/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.sln b/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.sln deleted file mode 100644 index 87d2afb..0000000 --- a/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.sln +++ /dev/null @@ -1,34 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26124.0 -MinimumVisualStudioVersion = 15.0.26124.0 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet_hooks_sln_repo", "dotnet_hooks_sln_repo.csproj", "{6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x64.ActiveCfg = Debug|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x64.Build.0 = Debug|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x86.ActiveCfg = Debug|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x86.Build.0 = Debug|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|Any CPU.Build.0 = Release|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x64.ActiveCfg = Release|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x64.Build.0 = Release|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x86.ActiveCfg = Release|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal diff --git a/testing/resources/golang_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/golang_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 206733b..0000000 --- a/testing/resources/golang_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: golang-hook - name: golang example hook - entry: golang-hello-world - language: golang - files: '' diff --git a/testing/resources/golang_hooks_repo/go.mod b/testing/resources/golang_hooks_repo/go.mod deleted file mode 100644 index f37d4b6..0000000 --- a/testing/resources/golang_hooks_repo/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module golang-hello-world - -go 1.18 - -require github.com/BurntSushi/toml v1.1.0 diff --git a/testing/resources/golang_hooks_repo/go.sum b/testing/resources/golang_hooks_repo/go.sum deleted file mode 100644 index ec0c385..0000000 --- a/testing/resources/golang_hooks_repo/go.sum +++ /dev/null @@ -1,2 +0,0 @@ -github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= -github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= diff --git a/testing/resources/golang_hooks_repo/golang-hello-world/main.go b/testing/resources/golang_hooks_repo/golang-hello-world/main.go deleted file mode 100644 index 1685743..0000000 --- a/testing/resources/golang_hooks_repo/golang-hello-world/main.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - - -import ( - "fmt" - "runtime" - "github.com/BurntSushi/toml" - "os" -) - -type Config struct { - What string -} - -func main() { - message := runtime.Version() - if len(os.Args) > 1 { - message = os.Args[1] - } - var conf Config - toml.Decode("What = 'world'\n", &conf) - fmt.Printf("hello %v from %s\n", conf.What, message) -} diff --git a/testing/resources/python_venv_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/python_venv_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index a666ed8..0000000 --- a/testing/resources/python_venv_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: foo - name: Foo - entry: foo - language: python_venv - files: \.py$ diff --git a/testing/resources/python_venv_hooks_repo/foo.py b/testing/resources/python_venv_hooks_repo/foo.py deleted file mode 100644 index 40efde3..0000000 --- a/testing/resources/python_venv_hooks_repo/foo.py +++ /dev/null @@ -1,9 +0,0 @@ -from __future__ import annotations - -import sys - - -def main(): - print(repr(sys.argv[1:])) - print('Hello World') - return 0 diff --git a/testing/resources/python_venv_hooks_repo/setup.py b/testing/resources/python_venv_hooks_repo/setup.py deleted file mode 100644 index cff6cad..0000000 --- a/testing/resources/python_venv_hooks_repo/setup.py +++ /dev/null @@ -1,10 +0,0 @@ -from __future__ import annotations - -from setuptools import setup - -setup( - name='foo', - version='0.0.0', - py_modules=['foo'], - entry_points={'console_scripts': ['foo = foo:main']}, -) diff --git a/testing/util.py b/testing/util.py index b6c3804..7c68d0e 100644 --- a/testing/util.py +++ b/testing/util.py @@ -6,24 +6,13 @@ import subprocess import pytest -from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output -from pre_commit.util import cmd_output_b from testing.auto_namedtuple import auto_namedtuple TESTING_DIR = os.path.abspath(os.path.dirname(__file__)) -def docker_is_running() -> bool: # pragma: win32 no cover - try: - cmd_output_b('docker', 'ps') - except CalledProcessError: # pragma: no cover - return False - else: - return True - - def get_resource_path(path): return os.path.join(TESTING_DIR, 'resources', path) @@ -41,10 +30,6 @@ def cmd_output_mocked_pre_commit_home( return ret, out.replace('\r\n', '\n'), None -skipif_cant_run_docker = pytest.mark.skipif( - os.name == 'nt' or not docker_is_running(), - reason="Docker isn't running or can't be accessed", -) xfailif_windows = pytest.mark.xfail(os.name == 'nt', reason='windows') diff --git a/tests/all_languages_test.py b/tests/all_languages_test.py new file mode 100644 index 0000000..98c9121 --- /dev/null +++ b/tests/all_languages_test.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from pre_commit.all_languages import languages + + +def test_python_venv_is_an_alias_to_python(): + assert languages['python_venv'] is languages['python'] diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index fca1ad9..ba18463 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -134,6 +134,39 @@ def test_migrate_config_sha_to_rev(tmpdir): ) +def test_migrate_config_language_python_venv(tmp_path): + src = '''\ +repos: +- repo: local + hooks: + - id: example + name: example + entry: example + language: python_venv + - id: example + name: example + entry: example + language: system +''' + expected = '''\ +repos: +- repo: local + hooks: + - id: example + name: example + entry: example + language: python + - id: example + name: example + entry: example + language: system +''' + cfg = tmp_path.joinpath('cfg.yaml') + cfg.write_text(src) + assert migrate_config(str(cfg)) == 0 + assert cfg.read_text() == expected + + def test_migrate_config_invalid_yaml(tmpdir): contents = '[' cfg = tmpdir.join(C.CONFIG_FILE) diff --git a/tests/lang_base_test.py b/tests/lang_base_test.py new file mode 100644 index 0000000..a532b6a --- /dev/null +++ b/tests/lang_base_test.py @@ -0,0 +1,173 @@ +from __future__ import annotations + +import multiprocessing +import os.path +import sys +from unittest import mock + +import pytest + +import pre_commit.constants as C +from pre_commit import lang_base +from pre_commit import parse_shebang +from pre_commit.prefix import Prefix +from pre_commit.util import CalledProcessError + + +@pytest.fixture +def find_exe_mck(): + with mock.patch.object(parse_shebang, 'find_executable') as mck: + yield mck + + +@pytest.fixture +def homedir_mck(): + def fake_expanduser(pth): + assert pth == '~' + return os.path.normpath('/home/me') + + with mock.patch.object(os.path, 'expanduser', fake_expanduser): + yield + + +def test_exe_exists_does_not_exist(find_exe_mck, homedir_mck): + find_exe_mck.return_value = None + assert lang_base.exe_exists('ruby') is False + + +def test_exe_exists_exists(find_exe_mck, homedir_mck): + find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby') + assert lang_base.exe_exists('ruby') is True + + +def test_exe_exists_false_if_shim(find_exe_mck, homedir_mck): + find_exe_mck.return_value = os.path.normpath('/foo/shims/ruby') + assert lang_base.exe_exists('ruby') is False + + +def test_exe_exists_false_if_homedir(find_exe_mck, homedir_mck): + find_exe_mck.return_value = os.path.normpath('/home/me/somedir/ruby') + assert lang_base.exe_exists('ruby') is False + + +def test_exe_exists_commonpath_raises_ValueError(find_exe_mck, homedir_mck): + find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby') + with mock.patch.object(os.path, 'commonpath', side_effect=ValueError): + assert lang_base.exe_exists('ruby') is True + + +def test_exe_exists_true_when_homedir_is_slash(find_exe_mck): + find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby') + with mock.patch.object(os.path, 'expanduser', return_value=os.sep): + assert lang_base.exe_exists('ruby') is True + + +def test_basic_get_default_version(): + assert lang_base.basic_get_default_version() == C.DEFAULT + + +def test_basic_health_check(): + assert lang_base.basic_health_check(Prefix('.'), 'default') is None + + +def test_failed_setup_command_does_not_unicode_error(): + script = ( + 'import sys\n' + "sys.stderr.buffer.write(b'\\x81\\xfe')\n" + 'raise SystemExit(1)\n' + ) + + # an assertion that this does not raise `UnicodeError` + with pytest.raises(CalledProcessError): + lang_base.setup_cmd(Prefix('.'), (sys.executable, '-c', script)) + + +def test_environment_dir(tmp_path): + ret = lang_base.environment_dir(Prefix(tmp_path), 'langenv', 'default') + assert ret == f'{tmp_path}{os.sep}langenv-default' + + +def test_assert_version_default(): + with pytest.raises(AssertionError) as excinfo: + lang_base.assert_version_default('lang', '1.2.3') + msg, = excinfo.value.args + assert msg == ( + 'for now, pre-commit requires system-installed lang -- ' + 'you selected `language_version: 1.2.3`' + ) + + +def test_assert_no_additional_deps(): + with pytest.raises(AssertionError) as excinfo: + lang_base.assert_no_additional_deps('lang', ['hmmm']) + msg, = excinfo.value.args + assert msg == ( + 'for now, pre-commit does not support additional_dependencies for ' + 'lang -- ' + "you selected `additional_dependencies: ['hmmm']`" + ) + + +def test_no_env_noop(tmp_path): + before = os.environ.copy() + with lang_base.no_env(Prefix(tmp_path), '1.2.3'): + inside = os.environ.copy() + after = os.environ.copy() + assert before == inside == after + + +def test_target_concurrency_normal(): + with mock.patch.object(multiprocessing, 'cpu_count', return_value=123): + with mock.patch.dict(os.environ, {}, clear=True): + assert lang_base.target_concurrency() == 123 + + +def test_target_concurrency_testing_env_var(): + with mock.patch.dict( + os.environ, {'PRE_COMMIT_NO_CONCURRENCY': '1'}, clear=True, + ): + assert lang_base.target_concurrency() == 1 + + +def test_target_concurrency_on_travis(): + with mock.patch.dict(os.environ, {'TRAVIS': '1'}, clear=True): + assert lang_base.target_concurrency() == 2 + + +def test_target_concurrency_cpu_count_not_implemented(): + with mock.patch.object( + multiprocessing, 'cpu_count', side_effect=NotImplementedError, + ): + with mock.patch.dict(os.environ, {}, clear=True): + assert lang_base.target_concurrency() == 1 + + +def test_shuffled_is_deterministic(): + seq = [str(i) for i in range(10)] + expected = ['4', '0', '5', '1', '8', '6', '2', '3', '7', '9'] + assert lang_base._shuffled(seq) == expected + + +def test_xargs_require_serial_is_not_shuffled(): + ret, out = lang_base.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' + + +def test_basic_run_hook(tmp_path): + ret, out = lang_base.basic_run_hook( + Prefix(tmp_path), + 'echo hi', + ['hello'], + ['file', 'file', 'file'], + is_local=False, + require_serial=False, + color=False, + ) + assert ret == 0 + out = out.replace(b'\r\n', b'\n') + assert out == b'hi hello file file file\n' diff --git a/tests/languages/docker_image_test.py b/tests/languages/docker_image_test.py new file mode 100644 index 0000000..7993c11 --- /dev/null +++ b/tests/languages/docker_image_test.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from pre_commit.languages import docker_image +from testing.language_helpers import run_language +from testing.util import xfailif_windows + + +@xfailif_windows # pragma: win32 no cover +def test_docker_image_hook_via_entrypoint(tmp_path): + ret = run_language( + tmp_path, + docker_image, + '--entrypoint echo ubuntu:22.04', + args=('hello hello world',), + ) + assert ret == (0, b'hello hello world\n') + + +@xfailif_windows # pragma: win32 no cover +def test_docker_image_hook_via_args(tmp_path): + ret = run_language( + tmp_path, + docker_image, + 'ubuntu:22.04 echo', + args=('hello hello world',), + ) + assert ret == (0, b'hello hello world\n') diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 5f7c85e..836382a 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -11,6 +11,8 @@ import pytest from pre_commit.languages import docker from pre_commit.util import CalledProcessError +from testing.language_helpers import run_language +from testing.util import xfailif_windows DOCKER_CGROUP_EXAMPLE = b'''\ 12:hugetlb:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 @@ -181,3 +183,15 @@ def test_get_docker_path_in_docker_docker_in_docker(in_docker): err = CalledProcessError(1, (), b'', b'') with mock.patch.object(docker, 'cmd_output_b', side_effect=err): assert docker._get_docker_path('/project') == '/project' + + +@xfailif_windows # pragma: win32 no cover +def test_docker_hook(tmp_path): + dockerfile = '''\ +FROM ubuntu:22.04 +CMD ["echo", "This is overwritten by the entry"'] +''' + tmp_path.joinpath('Dockerfile').write_text(dockerfile) + + ret = run_language(tmp_path, docker, 'echo hello hello world') + assert ret == (0, b'hello hello world\n') diff --git a/tests/languages/dotnet_test.py b/tests/languages/dotnet_test.py index e69de29..470c03b 100644 --- a/tests/languages/dotnet_test.py +++ b/tests/languages/dotnet_test.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +from pre_commit.languages import dotnet +from testing.language_helpers import run_language + + +def _write_program_cs(tmp_path): + program_cs = '''\ +using System; + +namespace dotnet_tests +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello from dotnet!"); + } + } +} +''' + tmp_path.joinpath('Program.cs').write_text(program_cs) + + +def _csproj(tool_name): + return f'''\ + + + Exe + net6 + true + {tool_name} + ./nupkg + + +''' + + +def test_dotnet_csproj(tmp_path): + csproj = _csproj('testeroni') + _write_program_cs(tmp_path) + tmp_path.joinpath('dotnet_csproj.csproj').write_text(csproj) + ret = run_language(tmp_path, dotnet, 'testeroni') + assert ret == (0, b'Hello from dotnet!\n') + + +def test_dotnet_csproj_prefix(tmp_path): + csproj = _csproj('testeroni.tool') + _write_program_cs(tmp_path) + tmp_path.joinpath('dotnet_hooks_csproj_prefix.csproj').write_text(csproj) + ret = run_language(tmp_path, dotnet, 'testeroni.tool') + assert ret == (0, b'Hello from dotnet!\n') + + +def test_dotnet_sln(tmp_path): + csproj = _csproj('testeroni') + sln = '''\ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet_hooks_sln_repo", "dotnet_hooks_sln_repo.csproj", "{6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x64.ActiveCfg = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x64.Build.0 = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x86.ActiveCfg = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x86.Build.0 = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|Any CPU.Build.0 = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x64.ActiveCfg = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x64.Build.0 = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x86.ActiveCfg = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal +''' # noqa: E501 + _write_program_cs(tmp_path) + tmp_path.joinpath('dotnet_hooks_sln_repo.csproj').write_text(csproj) + tmp_path.joinpath('dotnet_hooks_sln_repo.sln').write_text(sln) + + ret = run_language(tmp_path, dotnet, 'testeroni') + assert ret == (0, b'Hello from dotnet!\n') + + +def _setup_dotnet_combo(tmp_path): + sln = '''\ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "proj1", "proj1\\proj1.csproj", "{38A939C3-DEA4-47D7-9B75-0418C4249662}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "proj2", "proj2\\proj2.csproj", "{4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Release|Any CPU.Build.0 = Release|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal +''' # noqa: E501 + tmp_path.joinpath('dotnet_hooks_combo_repo.sln').write_text(sln) + + csproj1 = _csproj('proj1') + proj1 = tmp_path.joinpath('proj1') + proj1.mkdir() + proj1.joinpath('proj1.csproj').write_text(csproj1) + _write_program_cs(proj1) + + csproj2 = _csproj('proj2') + proj2 = tmp_path.joinpath('proj2') + proj2.mkdir() + proj2.joinpath('proj2.csproj').write_text(csproj2) + _write_program_cs(proj2) + + +def test_dotnet_combo_proj1(tmp_path): + _setup_dotnet_combo(tmp_path) + ret = run_language(tmp_path, dotnet, 'proj1') + assert ret == (0, b'Hello from dotnet!\n') + + +def test_dotnet_combo_proj2(tmp_path): + _setup_dotnet_combo(tmp_path) + ret = run_language(tmp_path, dotnet, 'proj2') + assert ret == (0, b'Hello from dotnet!\n') diff --git a/tests/languages/fail_test.py b/tests/languages/fail_test.py new file mode 100644 index 0000000..7c74886 --- /dev/null +++ b/tests/languages/fail_test.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from pre_commit.languages import fail +from testing.language_helpers import run_language + + +def test_fail_hooks(tmp_path): + ret = run_language( + tmp_path, + fail, + 'watch out for', + file_args=('bunnies',), + ) + assert ret == (1, b'watch out for\n\nbunnies\n') diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py index 7c04255..ec5a878 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -6,8 +6,11 @@ import pytest import re_assert import pre_commit.constants as C +from pre_commit import lang_base +from pre_commit.envcontext import envcontext from pre_commit.languages import golang -from pre_commit.languages import helpers +from pre_commit.store import _make_local_repo +from testing.language_helpers import run_language ACTUAL_GET_DEFAULT_VERSION = golang.get_default_version.__wrapped__ @@ -15,7 +18,7 @@ ACTUAL_GET_DEFAULT_VERSION = golang.get_default_version.__wrapped__ @pytest.fixture def exe_exists_mck(): - with mock.patch.object(helpers, 'exe_exists') as mck: + with mock.patch.object(lang_base, 'exe_exists') as mck: yield mck @@ -41,3 +44,93 @@ def test_golang_infer_go_version_default(): assert version != C.DEFAULT re_assert.Matches(r'^\d+\.\d+(?:\.\d+)?$').assert_matches(version) + + +def _make_hello_world(tmp_path): + go_mod = '''\ +module golang-hello-world + +go 1.18 + +require github.com/BurntSushi/toml v1.1.0 +''' + go_sum = '''\ +github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= +github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +''' # noqa: E501 + hello_world_go = '''\ +package main + + +import ( + "fmt" + "github.com/BurntSushi/toml" +) + +type Config struct { + What string +} + +func main() { + var conf Config + toml.Decode("What = 'world'\\n", &conf) + fmt.Printf("hello %v\\n", conf.What) +} +''' + tmp_path.joinpath('go.mod').write_text(go_mod) + tmp_path.joinpath('go.sum').write_text(go_sum) + mod_dir = tmp_path.joinpath('golang-hello-world') + mod_dir.mkdir() + main_file = mod_dir.joinpath('main.go') + main_file.write_text(hello_world_go) + + +def test_golang_system(tmp_path): + _make_hello_world(tmp_path) + + ret = run_language(tmp_path, golang, 'golang-hello-world') + assert ret == (0, b'hello world\n') + + +def test_golang_default_version(tmp_path): + _make_hello_world(tmp_path) + + ret = run_language( + tmp_path, + golang, + 'golang-hello-world', + version=C.DEFAULT, + ) + assert ret == (0, b'hello world\n') + + +def test_golang_versioned(tmp_path): + _make_local_repo(str(tmp_path)) + + ret, out = run_language( + tmp_path, + golang, + 'go version', + version='1.18.4', + ) + + assert ret == 0 + assert out.startswith(b'go version go1.18.4') + + +def test_local_golang_additional_deps(tmp_path): + _make_local_repo(str(tmp_path)) + + ret = run_language( + tmp_path, + golang, + 'hello', + deps=('golang.org/x/example/hello@latest',), + ) + + assert ret == (0, b'Hello, Go examples!\n') + + +def test_golang_hook_still_works_when_gobin_is_set(tmp_path): + with envcontext((('GOBIN', str(tmp_path.joinpath('gobin'))),)): + test_golang_system(tmp_path) diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py deleted file mode 100644 index c209e7e..0000000 --- a/tests/languages/helpers_test.py +++ /dev/null @@ -1,135 +0,0 @@ -from __future__ import annotations - -import multiprocessing -import os.path -import sys -from unittest import mock - -import pytest - -import pre_commit.constants as C -from pre_commit import parse_shebang -from pre_commit.languages import helpers -from pre_commit.prefix import Prefix -from pre_commit.util import CalledProcessError - - -@pytest.fixture -def find_exe_mck(): - with mock.patch.object(parse_shebang, 'find_executable') as mck: - yield mck - - -@pytest.fixture -def homedir_mck(): - def fake_expanduser(pth): - assert pth == '~' - return os.path.normpath('/home/me') - - with mock.patch.object(os.path, 'expanduser', fake_expanduser): - yield - - -def test_exe_exists_does_not_exist(find_exe_mck, homedir_mck): - find_exe_mck.return_value = None - assert helpers.exe_exists('ruby') is False - - -def test_exe_exists_exists(find_exe_mck, homedir_mck): - find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby') - assert helpers.exe_exists('ruby') is True - - -def test_exe_exists_false_if_shim(find_exe_mck, homedir_mck): - find_exe_mck.return_value = os.path.normpath('/foo/shims/ruby') - assert helpers.exe_exists('ruby') is False - - -def test_exe_exists_false_if_homedir(find_exe_mck, homedir_mck): - find_exe_mck.return_value = os.path.normpath('/home/me/somedir/ruby') - assert helpers.exe_exists('ruby') is False - - -def test_exe_exists_commonpath_raises_ValueError(find_exe_mck, homedir_mck): - find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby') - with mock.patch.object(os.path, 'commonpath', side_effect=ValueError): - assert helpers.exe_exists('ruby') is True - - -def test_exe_exists_true_when_homedir_is_slash(find_exe_mck): - find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby') - with mock.patch.object(os.path, 'expanduser', return_value=os.sep): - assert helpers.exe_exists('ruby') is True - - -def test_basic_get_default_version(): - assert helpers.basic_get_default_version() == C.DEFAULT - - -def test_basic_health_check(): - assert helpers.basic_health_check(Prefix('.'), 'default') is None - - -def test_failed_setup_command_does_not_unicode_error(): - script = ( - 'import sys\n' - "sys.stderr.buffer.write(b'\\x81\\xfe')\n" - 'raise SystemExit(1)\n' - ) - - # an assertion that this does not raise `UnicodeError` - with pytest.raises(CalledProcessError): - helpers.run_setup_cmd(Prefix('.'), (sys.executable, '-c', script)) - - -def test_assert_no_additional_deps(): - with pytest.raises(AssertionError) as excinfo: - helpers.assert_no_additional_deps('lang', ['hmmm']) - msg, = excinfo.value.args - assert msg == ( - 'for now, pre-commit does not support additional_dependencies for ' - 'lang -- ' - "you selected `additional_dependencies: ['hmmm']`" - ) - - -def test_target_concurrency_normal(): - with mock.patch.object(multiprocessing, 'cpu_count', return_value=123): - with mock.patch.dict(os.environ, {}, clear=True): - assert helpers.target_concurrency() == 123 - - -def test_target_concurrency_testing_env_var(): - with mock.patch.dict( - os.environ, {'PRE_COMMIT_NO_CONCURRENCY': '1'}, clear=True, - ): - assert helpers.target_concurrency() == 1 - - -def test_target_concurrency_on_travis(): - with mock.patch.dict(os.environ, {'TRAVIS': '1'}, clear=True): - assert helpers.target_concurrency() == 2 - - -def test_target_concurrency_cpu_count_not_implemented(): - with mock.patch.object( - multiprocessing, 'cpu_count', side_effect=NotImplementedError, - ): - with mock.patch.dict(os.environ, {}, clear=True): - 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/pygrep_test.py b/tests/languages/pygrep_test.py index 8420046..c6271c8 100644 --- a/tests/languages/pygrep_test.py +++ b/tests/languages/pygrep_test.py @@ -3,6 +3,7 @@ from __future__ import annotations import pytest from pre_commit.languages import pygrep +from testing.language_helpers import run_language @pytest.fixture @@ -13,6 +14,9 @@ def some_files(tmpdir): tmpdir.join('f4').write_binary(b'foo\npattern\nbar\n') tmpdir.join('f5').write_binary(b'[INFO] hi\npattern\nbar') tmpdir.join('f6').write_binary(b"pattern\nbarwith'foo\n") + tmpdir.join('f7').write_binary(b"hello'hi\nworld\n") + tmpdir.join('f8').write_binary(b'foo\nbar\nbaz\n') + tmpdir.join('f9').write_binary(b'[WARN] hi\n') with tmpdir.as_cwd(): yield @@ -125,3 +129,16 @@ def test_multiline_multiline_flag_is_enabled(cap_out): out = cap_out.get() assert ret == 1 assert out == 'f1:1:foo\nbar\n' + + +def test_grep_hook_matching(some_files, tmp_path): + ret = run_language( + tmp_path, pygrep, 'ello', file_args=('f7', 'f8', 'f9'), + ) + assert ret == (1, b"f7:1:hello'hi\n") + + +@pytest.mark.parametrize('regex', ('nope', "foo'bar", r'^\[INFO\]')) +def test_grep_hook_not_matching(regex, some_files, tmp_path): + ret = run_language(tmp_path, pygrep, regex, file_args=('f7', 'f8', 'f9')) + assert ret == (0, b'') diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 54fb98f..8bb284e 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -12,6 +12,7 @@ from pre_commit.languages import python from pre_commit.prefix import Prefix from pre_commit.util import make_executable from pre_commit.util import win_exe +from testing.language_helpers import run_language def test_read_pyvenv_cfg(tmpdir): @@ -210,3 +211,25 @@ def test_unhealthy_then_replaced(python_dir): os.replace(f'{py_exe}.tmp', py_exe) assert python.health_check(prefix, C.DEFAULT) is None + + +def test_language_versioned_python_hook(tmp_path): + setup_py = '''\ +from setuptools import setup +setup( + name='example', + py_modules=['mod'], + entry_points={'console_scripts': ['myexe=mod:main']}, +) +''' + tmp_path.joinpath('setup.py').write_text(setup_py) + tmp_path.joinpath('mod.py').write_text('def main(): print("ohai")') + + # we patch this to force virtualenv executing with `-p` since we can't + # reliably have multiple pythons available in CI + with mock.patch.object( + python, + '_sys_executable_matches', + return_value=False, + ): + assert run_language(tmp_path, python, 'myexe') == (0, b'ohai\n') diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 9cfaad5..6397a43 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -9,8 +9,8 @@ import pre_commit.constants as C from pre_commit import parse_shebang from pre_commit.envcontext import envcontext from pre_commit.languages import ruby +from pre_commit.languages.ruby import _resource_bytesio from pre_commit.store import _make_local_repo -from pre_commit.util import resource_bytesio from testing.language_helpers import run_language from testing.util import cwd from testing.util import xfailif_windows @@ -40,7 +40,7 @@ def test_uses_system_if_both_gem_and_ruby_are_available(find_exe_mck): ('rbenv.tar.gz', 'ruby-build.tar.gz', 'ruby-download.tar.gz'), ) def test_archive_root_stat(filename): - with resource_bytesio(filename) as f: + with _resource_bytesio(filename) as f: with tarfile.open(fileobj=f) as tarf: root, _, _ = filename.partition('.') assert oct(tarf.getmember(root).mode) == '0o755' diff --git a/tests/languages/script_test.py b/tests/languages/script_test.py new file mode 100644 index 0000000..a02f615 --- /dev/null +++ b/tests/languages/script_test.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from pre_commit.languages import script +from pre_commit.util import make_executable +from testing.language_helpers import run_language + + +def test_script_language(tmp_path): + exe = tmp_path.joinpath('main') + exe.write_text('#!/usr/bin/env bash\necho hello hello world\n') + make_executable(exe) + + expected = (0, b'hello hello world\n') + assert run_language(tmp_path, script, 'main') == expected diff --git a/tests/languages/system_test.py b/tests/languages/system_test.py new file mode 100644 index 0000000..dcd9cf1 --- /dev/null +++ b/tests/languages/system_test.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from pre_commit.languages import system +from testing.language_helpers import run_language + + +def test_system_language(tmp_path): + expected = (0, b'hello hello world\n') + assert run_language(tmp_path, system, 'echo hello hello world') == expected diff --git a/tests/repository_test.py b/tests/repository_test.py index b43b344..9e5d9d6 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -10,15 +10,12 @@ import pytest import re_assert import pre_commit.constants as C -from pre_commit import git +from pre_commit import lang_base +from pre_commit.all_languages import languages from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest -from pre_commit.envcontext import envcontext from pre_commit.hook import Hook -from pre_commit.languages import golang -from pre_commit.languages import helpers from pre_commit.languages import python -from pre_commit.languages.all import languages from pre_commit.prefix import Prefix from pre_commit.repository import _hook_installed from pre_commit.repository import all_hooks @@ -28,26 +25,24 @@ from pre_commit.util import cmd_output_b from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo from testing.fixtures import modify_manifest +from testing.language_helpers import run_language from testing.util import cwd from testing.util import get_resource_path -from testing.util import skipif_cant_run_docker - - -def _norm_out(b): - return b.replace(b'\r\n', b'\n') def _hook_run(hook, filenames, color): - with languages[hook.language].in_env(hook.prefix, hook.language_version): - return languages[hook.language].run_hook( - hook.prefix, - hook.entry, - hook.args, - filenames, - is_local=hook.src == 'local', - require_serial=hook.require_serial, - color=color, - ) + return run_language( + path=hook.prefix.prefix_dir, + language=languages[hook.language], + exe=hook.entry, + args=hook.args, + file_args=filenames, + version=hook.language_version, + deps=hook.additional_dependencies, + is_local=hook.src == 'local', + require_serial=hook.require_serial, + color=color, + ) def _get_hook_no_install(repo_config, store, hook_id): @@ -81,7 +76,7 @@ def _test_hook_repo( hook = _get_hook(config, store, hook_id) ret, out = _hook_run(hook, args, color=color) assert ret == expected_return_code - assert _norm_out(out) == expected + assert out == expected def test_python_hook(tempdir_factory, store): @@ -129,66 +124,21 @@ def test_python_hook_weird_setup_cfg(in_git_dir, tempdir_factory, store): ) -def test_python_venv(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'python_venv_hooks_repo', - 'foo', [os.devnull], - f'[{os.devnull!r}]\nHello World\n'.encode(), - ) - - -def test_language_versioned_python_hook(tempdir_factory, store): - # we patch this force virtualenv executing with `-p` since we can't - # reliably have multiple pythons available in CI - with mock.patch.object( - python, - '_sys_executable_matches', - return_value=False, - ): - _test_hook_repo( - tempdir_factory, store, 'python3_hooks_repo', - 'python3-hook', - [os.devnull], - f'3\n[{os.devnull!r}]\nHello World\n'.encode(), - ) - - -@skipif_cant_run_docker # pragma: win32 no cover -def test_run_a_docker_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'docker_hooks_repo', - 'docker-hook', - ['Hello World from docker'], b'Hello World from docker\n', - ) - - -@skipif_cant_run_docker # pragma: win32 no cover -def test_run_a_docker_hook_with_entry_args(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'docker_hooks_repo', - 'docker-hook-arg', - ['Hello World from docker'], b'Hello World from docker', - ) - - -@skipif_cant_run_docker # pragma: win32 no cover -def test_run_a_failing_docker_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'docker_hooks_repo', - 'docker-hook-failing', - ['Hello World from docker'], - mock.ANY, # an error message about `bork` not existing - expected_return_code=127, - ) - - -@skipif_cant_run_docker # pragma: win32 no cover -@pytest.mark.parametrize('hook_id', ('echo-entrypoint', 'echo-cmd')) -def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): - _test_hook_repo( - tempdir_factory, store, 'docker_image_hooks_repo', - hook_id, - ['Hello World from docker'], b'Hello World from docker\n', +def test_python_venv_deprecation(store, caplog): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'example', + 'name': 'example', + 'language': 'python_venv', + 'entry': 'echo hi', + }], + } + _get_hook(config, store, 'example') + assert caplog.messages[-1] == ( + '`repo: local` uses deprecated `language: python_venv`. ' + 'This is an alias for `language: python`. ' + 'Often `pre-commit autoupdate --repo local` will fix this.' ) @@ -199,92 +149,6 @@ def test_system_hook_with_spaces(tempdir_factory, store): ) -def test_golang_system_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'golang_hooks_repo', - 'golang-hook', ['system'], b'hello world from system\n', - config_kwargs={ - 'hooks': [{ - 'id': 'golang-hook', - 'language_version': 'system', - }], - }, - ) - - -def test_golang_versioned_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'golang_hooks_repo', - 'golang-hook', [], b'hello world from go1.18.4\n', - config_kwargs={ - 'hooks': [{ - 'id': 'golang-hook', - 'language_version': '1.18.4', - }], - }, - ) - - -def test_golang_hook_still_works_when_gobin_is_set(tempdir_factory, store): - gobin_dir = tempdir_factory.get() - with envcontext((('GOBIN', gobin_dir),)): - test_golang_system_hook(tempdir_factory, store) - assert os.listdir(gobin_dir) == [] - - -def test_golang_with_recursive_submodule(tmpdir, tempdir_factory, store): - sub_go = '''\ -package sub - -import "fmt" - -func Func() { - fmt.Println("hello hello world") -} -''' - sub = tmpdir.join('sub').ensure_dir() - sub.join('sub.go').write(sub_go) - cmd_output('git', '-C', str(sub), 'init', '.') - cmd_output('git', '-C', str(sub), 'add', '.') - git.commit(str(sub)) - - pre_commit_hooks = '''\ -- id: example - name: example - entry: example - language: golang - verbose: true -''' - go_mod = '''\ -module github.com/asottile/example - -go 1.14 -''' - main_go = '''\ -package main - -import "github.com/asottile/example/sub" - -func main() { - sub.Func() -} -''' - repo = tmpdir.join('repo').ensure_dir() - repo.join('.pre-commit-hooks.yaml').write(pre_commit_hooks) - repo.join('go.mod').write(go_mod) - repo.join('main.go').write(main_go) - cmd_output('git', '-C', str(repo), 'init', '.') - cmd_output('git', '-C', str(repo), 'add', '.') - cmd_output('git', '-C', str(repo), 'submodule', 'add', str(sub), 'sub') - git.commit(str(repo)) - - config = make_config_from_repo(str(repo)) - hook = _get_hook(config, store, 'example') - ret, out = _hook_run(hook, (), color=False) - assert ret == 0 - assert _norm_out(out) == b'hello hello world\n' - - def test_missing_executable(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'not_found_exe', @@ -345,52 +209,6 @@ def test_output_isatty(tempdir_factory, store): ) -def _make_grep_repo(entry, store, args=()): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'grep-hook', - 'name': 'grep-hook', - 'language': 'pygrep', - 'entry': entry, - 'args': args, - 'types': ['text'], - }], - } - return _get_hook(config, store, 'grep-hook') - - -@pytest.fixture -def greppable_files(tmpdir): - with tmpdir.as_cwd(): - cmd_output_b('git', 'init', '.') - tmpdir.join('f1').write_binary(b"hello'hi\nworld\n") - tmpdir.join('f2').write_binary(b'foo\nbar\nbaz\n') - tmpdir.join('f3').write_binary(b'[WARN] hi\n') - yield tmpdir - - -def test_grep_hook_matching(greppable_files, store): - hook = _make_grep_repo('ello', store) - ret, out = _hook_run(hook, ('f1', 'f2', 'f3'), color=False) - assert ret == 1 - assert _norm_out(out) == b"f1:1:hello'hi\n" - - -def test_grep_hook_case_insensitive(greppable_files, store): - hook = _make_grep_repo('ELLO', store, args=['-i']) - ret, out = _hook_run(hook, ('f1', 'f2', 'f3'), color=False) - assert ret == 1 - assert _norm_out(out) == b"f1:1:hello'hi\n" - - -@pytest.mark.parametrize('regex', ('nope', "foo'bar", r'^\[INFO\]')) -def test_grep_hook_not_matching(regex, greppable_files, store): - hook = _make_grep_repo(regex, store) - ret, out = _hook_run(hook, ('f1', 'f2', 'f3'), color=False) - assert (ret, out) == (0, b'') - - def _norm_pwd(path): # Under windows bash's temp and windows temp is different. # This normalizes to the bash /tmp @@ -440,7 +258,7 @@ def test_repository_state_compatibility(tempdir_factory, store, v): config = make_config_from_repo(path) hook = _get_hook(config, store, 'foo') - envdir = helpers.environment_dir( + envdir = lang_base.environment_dir( hook.prefix, python.ENVIRONMENT_DIR, hook.language_version, @@ -449,67 +267,6 @@ def test_repository_state_compatibility(tempdir_factory, store, v): assert _hook_installed(hook) is True -def test_additional_golang_dependencies_installed( - tempdir_factory, store, -): - path = make_repo(tempdir_factory, 'golang_hooks_repo') - config = make_config_from_repo(path) - # A small go package - deps = ['golang.org/x/example/hello@latest'] - config['hooks'][0]['additional_dependencies'] = deps - hook = _get_hook(config, store, 'golang-hook') - envdir = helpers.environment_dir( - hook.prefix, - golang.ENVIRONMENT_DIR, - golang.get_default_version(), - ) - binaries = os.listdir(os.path.join(envdir, 'bin')) - # normalize for windows - binaries = [os.path.splitext(binary)[0] for binary in binaries] - assert 'hello' in binaries - - -def test_local_golang_additional_dependencies(store): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'hello', - 'name': 'hello', - 'entry': 'hello', - 'language': 'golang', - 'additional_dependencies': ['golang.org/x/example/hello@latest'], - }], - } - hook = _get_hook(config, store, 'hello') - ret, out = _hook_run(hook, (), color=False) - assert ret == 0 - assert _norm_out(out) == b'Hello, Go examples!\n' - - -def test_fail_hooks(store): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'fail', - 'name': 'fail', - 'language': 'fail', - 'entry': 'make sure to name changelogs as .rst!', - 'files': r'changelog/.*(?