diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/00_bug.yaml
similarity index 87%
rename from .github/ISSUE_TEMPLATE/bug.yaml
rename to .github/ISSUE_TEMPLATE/00_bug.yaml
index 96cd6c75..980f7afe 100644
--- a/.github/ISSUE_TEMPLATE/bug.yaml
+++ b/.github/ISSUE_TEMPLATE/00_bug.yaml
@@ -16,6 +16,12 @@ body:
placeholder: ...
validations:
required: true
+ - type: markdown
+ attributes:
+ value: |
+ 95% of issues created are duplicates.
+ please try extra hard to find them first.
+ it's very unlikely your problem is unique.
- type: textarea
id: freeform
attributes:
diff --git a/.github/ISSUE_TEMPLATE/01_feature.yaml b/.github/ISSUE_TEMPLATE/01_feature.yaml
new file mode 100644
index 00000000..c7ddc84c
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/01_feature.yaml
@@ -0,0 +1,38 @@
+name: feature request
+description: something new
+body:
+ - type: markdown
+ attributes:
+ value: |
+ this is for issues for `pre-commit` (the framework).
+ if you are reporting an issue for [pre-commit.ci] please report it at [pre-commit-ci/issues]
+
+ [pre-commit.ci]: https://pre-commit.ci
+ [pre-commit-ci/issues]: https://github.com/pre-commit-ci/issues
+ - type: input
+ id: search
+ attributes:
+ label: search you tried in the issue tracker
+ placeholder: ...
+ validations:
+ required: true
+ - type: markdown
+ attributes:
+ value: |
+ 95% of issues created are duplicates.
+ please try extra hard to find them first.
+ it's very unlikely your feature idea is a new one.
+ - type: textarea
+ id: freeform
+ attributes:
+ label: describe your actual problem
+ placeholder: 'I want to do ... I tried ... It does not work because ...'
+ validations:
+ required: true
+ - type: input
+ id: version
+ attributes:
+ label: pre-commit --version
+ placeholder: pre-commit x.x.x
+ validations:
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000..4179f47f
--- /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 608c0cd1..b70c942f 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.2.0
- if: inputs.env == 'py38' && runner.os == 'Linux'
+ - uses: asottile/workflows/.github/actions/latest-git@v1.4.0
+ if: inputs.env == 'py39' && runner.os == 'Linux'
diff --git a/.github/workflows/languages.yaml b/.github/workflows/languages.yaml
new file mode 100644
index 00000000..be8963ba
--- /dev/null
+++ b/.github/workflows/languages.yaml
@@ -0,0 +1,84 @@
+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.10'
+ - 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.8.1
+ - uses: actions/setup-python@v4
+ with:
+ python-version: '3.10'
+
+ - 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'
+ - uses: haskell/actions/setup@v2
+ if: matrix.language == 'haskell'
+ - uses: r-lib/actions/setup-r@v2
+ if: matrix.os == 'ubuntu-latest' && matrix.language == 'r'
+
+ - name: install deps
+ run: python -mpip install -e . -r requirements-dev.txt
+ - 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/.github/workflows/main.yml b/.github/workflows/main.yml
index c78d1051..02b11ae2 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -3,7 +3,7 @@ name: main
on:
push:
branches: [main, test-me-*]
- tags:
+ tags: '*'
pull_request:
concurrency:
@@ -12,12 +12,12 @@ concurrency:
jobs:
main-windows:
- uses: asottile/workflows/.github/workflows/tox.yml@v1.2.0
+ uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1
with:
- env: '["py38"]'
+ env: '["py310"]'
os: windows-latest
main-linux:
- uses: asottile/workflows/.github/workflows/tox.yml@v1.2.0
+ uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1
with:
- env: '["py38", "py39", "py310"]'
+ env: '["py310", "py311", "py312", "py313"]'
os: ubuntu-latest
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index b7d7f1f0..3654066f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.4.0
+ rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
@@ -10,36 +10,35 @@ repos:
- id: name-tests-test
- id: requirements-txt-fixer
- repo: https://github.com/asottile/setup-cfg-fmt
- rev: v2.2.0
+ rev: v3.2.0
hooks:
- id: setup-cfg-fmt
-- repo: https://github.com/asottile/reorder_python_imports
- rev: v3.9.0
+- repo: https://github.com/asottile/reorder-python-imports
+ rev: v3.16.0
hooks:
- id: reorder-python-imports
- exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/)
- args: [--py38-plus, --add-import, 'from __future__ import annotations']
+ exclude: ^pre_commit/resources/
+ args: [--py310-plus, --add-import, 'from __future__ import annotations']
- repo: https://github.com/asottile/add-trailing-comma
- rev: v2.4.0
+ rev: v4.0.0
hooks:
- id: add-trailing-comma
- args: [--py36-plus]
- repo: https://github.com/asottile/pyupgrade
- rev: v3.3.1
+ rev: v3.21.2
hooks:
- id: pyupgrade
- args: [--py38-plus]
-- repo: https://github.com/pre-commit/mirrors-autopep8
- rev: v2.0.1
+ args: [--py310-plus]
+- repo: https://github.com/hhatto/autopep8
+ rev: v2.3.2
hooks:
- id: autopep8
- repo: https://github.com/PyCQA/flake8
- rev: 6.0.0
+ rev: 7.3.0
hooks:
- id: flake8
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: v0.991
+ rev: v1.19.1
hooks:
- id: mypy
- additional_dependencies: [types-all]
+ additional_dependencies: [types-pyyaml]
exclude: ^testing/resources/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c0657e63..879ae073 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,331 @@
+4.5.1 - 2025-12-16
+==================
+
+### Fixes
+- Fix `language: python` with `repo: local` without `additional_dependencies`.
+ - #3597 PR by @asottile.
+
+4.5.0 - 2025-11-22
+==================
+
+### Features
+- Add `pre-commit hazmat`.
+ - #3585 PR by @asottile.
+
+4.4.0 - 2025-11-08
+==================
+
+### Features
+- Add `--fail-fast` option to `pre-commit run`.
+ - #3528 PR by @JulianMaurin.
+- Upgrade `ruby-build` / `rbenv`.
+ - #3566 PR by @asottile.
+ - #3565 issue by @MRigal.
+- Add `language: unsupported` / `language: unsupported_script` as aliases
+ for `language: system` / `language: script` (which will eventually be
+ deprecated).
+ - #3577 PR by @asottile.
+- Add support docker-in-docker detection for cgroups v2.
+ - #3535 PR by @br-rhrbacek.
+ - #3360 issue by @JasonAlt.
+
+### Fixes
+- Handle when docker gives `SecurityOptions: null`.
+ - #3537 PR by @asottile.
+ - #3514 issue by @jenstroeger.
+- Fix error context for invalid `stages` in `.pre-commit-config.yaml`.
+ - #3576 PR by @asottile.
+
+4.3.0 - 2025-08-09
+==================
+
+### Features
+- `language: docker` / `language: docker_image`: detect rootless docker.
+ - #3446 PR by @matthewhughes934.
+ - #1243 issue by @dkolepp.
+- `language: julia`: avoid `startup.jl` when executing hooks.
+ - #3496 PR by @ericphanson.
+- `language: dart`: support latest dart versions which require a higher sdk
+ lower bound.
+ - #3507 PR by @bc-lee.
+
+4.2.0 - 2025-03-18
+==================
+
+### Features
+- For `language: python` first attempt a versioned python executable for
+ the default language version before consulting a potentially unversioned
+ `sys.executable`.
+ - #3430 PR by @asottile.
+
+### Fixes
+- Handle error during conflict detection when a file is named "HEAD"
+ - #3425 PR by @tusharsadhwani.
+
+4.1.0 - 2025-01-20
+==================
+
+### Features
+- Add `language: julia`.
+ - #3348 PR by @fredrikekre.
+ - #2689 issue @jmuchovej.
+
+### Fixes
+- Disable automatic toolchain switching for `language: golang`.
+ - #3304 PR by @AleksaC.
+ - #3300 issue by @AleksaC.
+ - #3149 issue by @nijel.
+- Fix `language: r` installation when initiated by RStudio.
+ - #3389 PR by @lorenzwalthert.
+ - #3385 issue by @lorenzwalthert.
+
+
+4.0.1 - 2024-10-08
+==================
+
+### Fixes
+- Fix `pre-commit migrate-config` for unquoted deprecated stages names with
+ purelib `pyyaml`.
+ - #3324 PR by @asottile.
+ - pre-commit-ci/issues#234 issue by @lorenzwalthert.
+
+4.0.0 - 2024-10-05
+==================
+
+### Features
+- Improve `pre-commit migrate-config` to handle more yaml formats.
+ - #3301 PR by @asottile.
+- Handle `stages` deprecation in `pre-commit migrate-config`.
+ - #3302 PR by @asottile.
+ - #2732 issue by @asottile.
+- Upgrade `ruby-build`.
+ - #3199 PR by @ThisGuyCodes.
+- Add "sensible regex" warnings to `repo: meta`.
+ - #3311 PR by @asottile.
+- Add warnings for deprecated `stages` (`commit` -> `pre-commit`, `push` ->
+ `pre-push`, `merge-commit` -> `pre-merge-commit`).
+ - #3312 PR by @asottile.
+ - #3313 PR by @asottile.
+ - #3315 PR by @asottile.
+ - #2732 issue by @asottile.
+
+### Updating
+- `language: python_venv` has been removed -- use `language: python` instead.
+ - #3320 PR by @asottile.
+ - #2734 issue by @asottile.
+
+3.8.0 - 2024-07-28
+==================
+
+### Features
+- Implement health checks for `language: r` so environments are recreated if
+ the system version of R changes.
+ - #3206 issue by @lorenzwalthert.
+ - #3265 PR by @lorenzwalthert.
+
+3.7.1 - 2024-05-10
+==================
+
+### Fixes
+- Fix `language: rust` default language version check when `rust-toolchain.toml`
+ is present.
+ - issue by @gaborbernat.
+ - #3201 PR by @asottile.
+
+3.7.0 - 2024-03-24
+==================
+
+### Features
+- Use a tty for `docker` and `docker_image` hooks when `--color` is specified.
+ - #3122 PR by @glehmann.
+
+### Fixes
+- Fix `fail_fast` for individual hooks stopping when previous hooks had failed.
+ - #3167 issue by @tp832944.
+ - #3168 PR by @asottile.
+
+### Updating
+- The per-hook behaviour of `fail_fast` was fixed. If you want the pre-3.7.0
+ behaviour, add `fail_fast: true` to all hooks before the last `fail_fast`
+ hook.
+
+3.6.2 - 2024-02-18
+==================
+
+### Fixes
+- Fix building golang hooks during `git commit --all`.
+ - #3130 PR by @asottile.
+ - #2722 issue by @pestanko and @matthewhughes934.
+
+3.6.1 - 2024-02-10
+==================
+
+### Fixes
+- Remove `PYTHONEXECUTABLE` from environment when running.
+ - #3110 PR by @untitaker.
+- Handle staged-files-only with only a crlf diff.
+ - #3126 PR by @asottile.
+ - issue by @tyyrok.
+
+3.6.0 - 2023-12-09
+==================
+
+### Features
+- Check `minimum_pre_commit_version` first when parsing configs.
+ - #3092 PR by @asottile.
+
+### Fixes
+- Fix deprecation warnings for `importlib.resources`.
+ - #3043 PR by @asottile.
+- Fix deprecation warnings for rmtree.
+ - #3079 PR by @edgarrmondragon.
+
+### Updating
+- Drop support for python<3.9.
+ - #3042 PR by @asottile.
+ - #3093 PR by @asottile.
+
+3.5.0 - 2023-10-13
+==================
+
+### Features
+- Improve performance of `check-hooks-apply` and `check-useless-excludes`.
+ - #2998 PR by @mxr.
+ - #2935 issue by @mxr.
+
+### Fixes
+- Use `time.monotonic()` for more accurate hook timing.
+ - #3024 PR by @adamchainz.
+
+### Updating
+- Require npm 6.x+ for `language: node` hooks.
+ - #2996 PR by @RoelAdriaans.
+ - #1983 issue by @henryiii.
+
+3.4.0 - 2023-09-02
+==================
+
+### Features
+- Add `language: haskell`.
+ - #2932 by @alunduil.
+- Improve cpu count detection when run under cgroups.
+ - #2979 PR by @jdb8.
+ - #2978 issue by @jdb8.
+
+### Fixes
+- Handle negative exit codes from hooks receiving posix signals.
+ - #2971 PR by @chriskuehl.
+ - #2970 issue by @chriskuehl.
+
+3.3.3 - 2023-06-13
+==================
+
+### Fixes
+- Work around OS packagers setting `--install-dir` / `--bin-dir` in gem settings.
+ - #2905 PR by @jaysoffian.
+ - #2799 issue by @lmilbaum.
+
+3.3.2 - 2023-05-17
+==================
+
+### Fixes
+- Work around `r` on windows sometimes double-un-quoting arguments.
+ - #2885 PR by @lorenzwalthert.
+ - #2870 issue by @lorenzwalthert.
+
+3.3.1 - 2023-05-02
+==================
+
+### Fixes
+- Work around `git` partial clone bug for `autoupdate` on windows.
+ - #2866 PR by @asottile.
+ - #2865 issue by @adehad.
+
+3.3.0 - 2023-05-01
+==================
+
+### Features
+- Upgrade ruby-build.
+ - #2846 PR by @jalessio.
+- Use blobless clone for faster autoupdate.
+ - #2859 PR by @asottile.
+- Add `-j` / `--jobs` argument to `autoupdate` for parallel execution.
+ - #2863 PR by @asottile.
+ - issue by @gaborbernat.
+
+3.2.2 - 2023-04-03
+==================
+
+### Fixes
+- Fix support for swift >= 5.8.
+ - #2836 PR by @edelabar.
+ - #2835 issue by @kgrobelny-intive.
+
+3.2.1 - 2023-03-25
+==================
+
+### Fixes
+- Fix `language_version` for `language: rust` without global `rustup`.
+ - #2823 issue by @daschuer.
+ - #2827 PR by @asottile.
+
+3.2.0 - 2023-03-17
+==================
+
+### Features
+- Allow `pre-commit`, `pre-push`, and `pre-merge-commit` as `stages`.
+ - #2732 issue by @asottile.
+ - #2808 PR by @asottile.
+- Add `pre-rebase` hook support.
+ - #2582 issue by @BrutalSimplicity.
+ - #2725 PR by @mgaligniana.
+
+### Fixes
+- Remove bulky cargo cache from `language: rust` installs.
+ - #2820 PR by @asottile.
+
+3.1.1 - 2023-02-27
+==================
+
+### Fixes
+- Fix `rust` with `language_version` and a non-writable host `RUSTUP_HOME`.
+ - pre-commit-ci/issues#173 by @Swiftb0y.
+ - #2788 by @asottile.
+
+3.1.0 - 2023-02-22
+==================
+
+### 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
+==================
+
+### Fixes
+- Fix hook diff detection for files affected by `--textconv`.
+ - #2743 PR by @adamchainz.
+ - #2743 issue by @adamchainz.
+
+3.0.3 - 2023-02-01
+==================
+
+### Fixes
+- Revert "Prevent local `Gemfile` from interfering with hook execution.".
+ - #2739 issue by @Roguelazer.
+ - #2740 PR by @asottile.
+
3.0.2 - 2023-01-29
==================
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a9bcb79e..da7f9432 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)
@@ -92,7 +92,7 @@ language, for example:
here are the apis that should be implemented for a language
-Note that these are also documented in [`pre_commit/languages/all.py`](https://github.com/pre-commit/pre-commit/blob/main/pre_commit/languages/all.py)
+Note that these are also documented in [`pre_commit/lang_base.py`](https://github.com/pre-commit/pre-commit/blob/main/pre_commit/lang_base.py)
#### `ENVIRONMENT_DIR`
@@ -111,7 +111,7 @@ one cannot be determined, return `'default'`.
You generally don't need to implement this on a first pass and can just use:
```python
-get_default_version = helpers.basic_default_version
+get_default_version = lang_base.basic_default_version
```
`python` is currently the only language which implements this api
@@ -125,7 +125,7 @@ healthy.
You generally don't need to implement this on a first pass and can just use:
```python
-health_check = helpers.basic_healthy_check
+health_check = lang_base.basic_health_check
```
`python` is currently the only language which implements this api, for python
@@ -137,7 +137,7 @@ this is the trickiest one to implement and where all the smart parts happen.
this api should do the following things
-- (0th / 3rd class): `install_environment = helpers.no_install`
+- (0th / 3rd class): `install_environment = lang_base.no_install`
- (1st class): install a language runtime into the hook's directory
- (2nd class): install the package at `.` into the `ENVIRONMENT_DIR`
- (2nd class, optional): install packages listed in `additional_dependencies`
diff --git a/pre_commit/all_languages.py b/pre_commit/all_languages.py
new file mode 100644
index 00000000..166bc167
--- /dev/null
+++ b/pre_commit/all_languages.py
@@ -0,0 +1,50 @@
+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 haskell
+from pre_commit.languages import julia
+from pre_commit.languages import lua
+from pre_commit.languages import node
+from pre_commit.languages import perl
+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 swift
+from pre_commit.languages import unsupported
+from pre_commit.languages import unsupported_script
+
+
+languages: dict[str, Language] = {
+ 'conda': conda,
+ 'coursier': coursier,
+ 'dart': dart,
+ 'docker': docker,
+ 'docker_image': docker_image,
+ 'dotnet': dotnet,
+ 'fail': fail,
+ 'golang': golang,
+ 'haskell': haskell,
+ 'julia': julia,
+ 'lua': lua,
+ 'node': node,
+ 'perl': perl,
+ 'pygrep': pygrep,
+ 'python': python,
+ 'r': r,
+ 'ruby': ruby,
+ 'rust': rust,
+ 'swift': swift,
+ 'unsupported': unsupported,
+ 'unsupported_script': unsupported_script,
+}
+language_names = sorted(languages)
diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py
index e191d3a0..51f14d26 100644
--- a/pre_commit/clientlib.py
+++ b/pre_commit/clientlib.py
@@ -2,24 +2,42 @@ from __future__ import annotations
import functools
import logging
+import os.path
import re
import shlex
import sys
+from collections.abc import Callable
+from collections.abc import Sequence
from typing import Any
-from typing import Sequence
+from typing import NamedTuple
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')
check_string_regex = cfgv.check_and(cfgv.check_string, cfgv.check_regex)
+HOOK_TYPES = (
+ 'commit-msg',
+ 'post-checkout',
+ 'post-commit',
+ 'post-merge',
+ 'post-rewrite',
+ 'pre-commit',
+ 'pre-merge-commit',
+ 'pre-push',
+ 'pre-rebase',
+ 'prepare-commit-msg',
+)
+# `manual` is not invoked by any installed git hook. See #719
+STAGES = (*HOOK_TYPES, 'manual')
+
def check_type_tag(tag: str) -> None:
if tag not in ALL_TAGS:
@@ -43,13 +61,186 @@ def check_min_version(version: str) -> None:
)
+_STAGES = {
+ 'commit': 'pre-commit',
+ 'merge-commit': 'pre-merge-commit',
+ 'push': 'pre-push',
+}
+
+
+def transform_stage(stage: str) -> str:
+ return _STAGES.get(stage, stage)
+
+
+MINIMAL_MANIFEST_SCHEMA = cfgv.Array(
+ cfgv.Map(
+ 'Hook', 'id',
+ cfgv.Required('id', cfgv.check_string),
+ cfgv.Optional('stages', cfgv.check_array(cfgv.check_string), []),
+ ),
+)
+
+
+def warn_for_stages_on_repo_init(repo: str, directory: str) -> None:
+ try:
+ manifest = cfgv.load_from_filename(
+ os.path.join(directory, C.MANIFEST_FILE),
+ schema=MINIMAL_MANIFEST_SCHEMA,
+ load_strategy=yaml_load,
+ exc_tp=InvalidManifestError,
+ )
+ except InvalidManifestError:
+ return # they'll get a better error message when it actually loads!
+
+ legacy_stages = {} # sorted set
+ for hook in manifest:
+ for stage in hook.get('stages', ()):
+ if stage in _STAGES:
+ legacy_stages[stage] = True
+
+ if legacy_stages:
+ logger.warning(
+ f'repo `{repo}` uses deprecated stage names '
+ f'({", ".join(legacy_stages)}) which will be removed in a '
+ f'future version. '
+ f'Hint: often `pre-commit autoupdate --repo {shlex.quote(repo)}` '
+ f'will fix this. '
+ f'if it does not -- consider reporting an issue to that repo.',
+ )
+
+
+class StagesMigrationNoDefault(NamedTuple):
+ key: str
+ default: Sequence[str]
+
+ def check(self, dct: dict[str, Any]) -> None:
+ if self.key not in dct:
+ return
+
+ with cfgv.validate_context(f'At key: {self.key}'):
+ val = dct[self.key]
+ cfgv.check_array(cfgv.check_any)(val)
+
+ val = [transform_stage(v) for v in val]
+ cfgv.check_array(cfgv.check_one_of(STAGES))(val)
+
+ def apply_default(self, dct: dict[str, Any]) -> None:
+ if self.key not in dct:
+ return
+ dct[self.key] = [transform_stage(v) for v in dct[self.key]]
+
+ def remove_default(self, dct: dict[str, Any]) -> None:
+ raise NotImplementedError
+
+
+class StagesMigration(StagesMigrationNoDefault):
+ def apply_default(self, dct: dict[str, Any]) -> None:
+ dct.setdefault(self.key, self.default)
+ super().apply_default(dct)
+
+
+class DeprecatedStagesWarning(NamedTuple):
+ key: str
+
+ def check(self, dct: dict[str, Any]) -> None:
+ if self.key not in dct:
+ return
+
+ val = dct[self.key]
+ cfgv.check_array(cfgv.check_any)(val)
+
+ legacy_stages = [stage for stage in val if stage in _STAGES]
+ if legacy_stages:
+ logger.warning(
+ f'hook id `{dct["id"]}` uses deprecated stage names '
+ f'({", ".join(legacy_stages)}) which will be removed in a '
+ f'future version. '
+ f'run: `pre-commit migrate-config` to automatically fix this.',
+ )
+
+ def apply_default(self, dct: dict[str, Any]) -> None:
+ pass
+
+ def remove_default(self, dct: dict[str, Any]) -> None:
+ raise NotImplementedError
+
+
+class DeprecatedDefaultStagesWarning(NamedTuple):
+ key: str
+
+ def check(self, dct: dict[str, Any]) -> None:
+ if self.key not in dct:
+ return
+
+ val = dct[self.key]
+ cfgv.check_array(cfgv.check_any)(val)
+
+ legacy_stages = [stage for stage in val if stage in _STAGES]
+ if legacy_stages:
+ logger.warning(
+ f'top-level `default_stages` uses deprecated stage names '
+ f'({", ".join(legacy_stages)}) which will be removed in a '
+ f'future version. '
+ f'run: `pre-commit migrate-config` to automatically fix this.',
+ )
+
+ def apply_default(self, dct: dict[str, Any]) -> None:
+ pass
+
+ def remove_default(self, dct: dict[str, Any]) -> None:
+ raise NotImplementedError
+
+
+def _translate_language(name: str) -> str:
+ return {
+ 'system': 'unsupported',
+ 'script': 'unsupported_script',
+ }.get(name, name)
+
+
+class LanguageMigration(NamedTuple): # remove
+ key: str
+ check_fn: Callable[[object], None]
+
+ def check(self, dct: dict[str, Any]) -> None:
+ if self.key not in dct:
+ return
+
+ with cfgv.validate_context(f'At key: {self.key}'):
+ self.check_fn(_translate_language(dct[self.key]))
+
+ def apply_default(self, dct: dict[str, Any]) -> None:
+ if self.key not in dct:
+ return
+
+ dct[self.key] = _translate_language(dct[self.key])
+
+ def remove_default(self, dct: dict[str, Any]) -> None:
+ raise NotImplementedError
+
+
+class LanguageMigrationRequired(LanguageMigration): # replace with Required
+ def check(self, dct: dict[str, Any]) -> None:
+ if self.key not in dct:
+ raise cfgv.ValidationError(f'Missing required key: {self.key}')
+
+ super().check(dct)
+
+
MANIFEST_HOOK_DICT = cfgv.Map(
'Hook', 'id',
+ # check first in case it uses some newer, incompatible feature
+ cfgv.Optional(
+ 'minimum_pre_commit_version',
+ cfgv.check_and(cfgv.check_string, check_min_version),
+ '0',
+ ),
+
cfgv.Required('id', cfgv.check_string),
cfgv.Required('name', cfgv.check_string),
cfgv.Required('entry', cfgv.check_string),
- cfgv.Required('language', cfgv.check_one_of(all_languages)),
+ LanguageMigrationRequired('language', cfgv.check_one_of(language_names)),
cfgv.Optional('alias', cfgv.check_string, ''),
cfgv.Optional('files', check_string_regex, ''),
@@ -68,9 +259,8 @@ MANIFEST_HOOK_DICT = cfgv.Map(
cfgv.Optional('description', cfgv.check_string, ''),
cfgv.Optional('language_version', cfgv.check_string, C.DEFAULT),
cfgv.Optional('log_file', cfgv.check_string, ''),
- cfgv.Optional('minimum_pre_commit_version', cfgv.check_string, '0'),
cfgv.Optional('require_serial', cfgv.check_bool, False),
- cfgv.Optional('stages', cfgv.check_array(cfgv.check_one_of(C.STAGES)), []),
+ StagesMigration('stages', []),
cfgv.Optional('verbose', cfgv.check_bool, False),
)
MANIFEST_SCHEMA = cfgv.Array(MANIFEST_HOOK_DICT)
@@ -80,10 +270,19 @@ class InvalidManifestError(FatalError):
pass
+def _load_manifest_forward_compat(contents: str) -> object:
+ obj = yaml_load(contents)
+ if isinstance(obj, dict):
+ check_min_version('5')
+ raise AssertionError('unreachable')
+ else:
+ return obj
+
+
load_manifest = functools.partial(
cfgv.load_from_filename,
schema=MANIFEST_SCHEMA,
- load_strategy=yaml_load,
+ load_strategy=_load_manifest_forward_compat,
exc_tp=InvalidManifestError,
)
@@ -205,12 +404,20 @@ class NotAllowed(cfgv.OptionalNoDefault):
raise cfgv.ValidationError(f'{self.key!r} cannot be overridden')
+_COMMON_HOOK_WARNINGS = (
+ OptionalSensibleRegexAtHook('files', cfgv.check_string),
+ OptionalSensibleRegexAtHook('exclude', cfgv.check_string),
+ DeprecatedStagesWarning('stages'),
+)
+
META_HOOK_DICT = cfgv.Map(
'Hook', 'id',
cfgv.Required('id', cfgv.check_string),
cfgv.Required('id', cfgv.check_one_of(tuple(k for k, _ in _meta))),
- # language must be system
- cfgv.Optional('language', cfgv.check_one_of({'system'}), 'system'),
+ # language must be `unsupported`
+ cfgv.Optional(
+ 'language', cfgv.check_one_of({'unsupported'}), 'unsupported',
+ ),
# entry cannot be overridden
NotAllowed('entry', cfgv.check_any),
*(
@@ -227,6 +434,7 @@ META_HOOK_DICT = cfgv.Map(
item
for item in MANIFEST_HOOK_DICT.items
),
+ *_COMMON_HOOK_WARNINGS,
)
CONFIG_HOOK_DICT = cfgv.Map(
'Hook', 'id',
@@ -241,17 +449,18 @@ CONFIG_HOOK_DICT = cfgv.Map(
cfgv.OptionalNoDefault(item.key, item.check_fn)
for item in MANIFEST_HOOK_DICT.items
if item.key != 'id'
+ if item.key != 'stages'
+ if item.key != 'language' # remove
),
- OptionalSensibleRegexAtHook('files', cfgv.check_string),
- OptionalSensibleRegexAtHook('exclude', cfgv.check_string),
+ StagesMigrationNoDefault('stages', []),
+ LanguageMigration('language', cfgv.check_one_of(language_names)), # remove
+ *_COMMON_HOOK_WARNINGS,
)
LOCAL_HOOK_DICT = cfgv.Map(
'Hook', 'id',
*MANIFEST_HOOK_DICT.items,
-
- OptionalSensibleRegexAtHook('files', cfgv.check_string),
- OptionalSensibleRegexAtHook('exclude', cfgv.check_string),
+ *_COMMON_HOOK_WARNINGS,
)
CONFIG_REPO_DICT = cfgv.Map(
'Repository', 'repo',
@@ -281,34 +490,33 @@ 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,
- cfgv.RequiredRecurse('repos', cfgv.Array(CONFIG_REPO_DICT)),
- cfgv.Optional(
- 'default_install_hook_types',
- cfgv.check_array(cfgv.check_one_of(C.HOOK_TYPES)),
- ['pre-commit'],
- ),
- cfgv.OptionalRecurse(
- 'default_language_version', DEFAULT_LANGUAGE_VERSION, {},
- ),
- cfgv.Optional(
- 'default_stages',
- cfgv.check_array(cfgv.check_one_of(C.STAGES)),
- C.STAGES,
- ),
- cfgv.Optional('files', check_string_regex, ''),
- cfgv.Optional('exclude', check_string_regex, '^$'),
- cfgv.Optional('fail_fast', cfgv.check_bool, False),
+ # check first in case it uses some newer, incompatible feature
cfgv.Optional(
'minimum_pre_commit_version',
cfgv.check_and(cfgv.check_string, check_min_version),
'0',
),
+
+ cfgv.RequiredRecurse('repos', cfgv.Array(CONFIG_REPO_DICT)),
+ cfgv.Optional(
+ 'default_install_hook_types',
+ cfgv.check_array(cfgv.check_one_of(HOOK_TYPES)),
+ ['pre-commit'],
+ ),
+ cfgv.OptionalRecurse(
+ 'default_language_version', DEFAULT_LANGUAGE_VERSION, {},
+ ),
+ StagesMigration('default_stages', STAGES),
+ DeprecatedDefaultStagesWarning('default_stages'),
+ cfgv.Optional('files', check_string_regex, ''),
+ cfgv.Optional('exclude', check_string_regex, '^$'),
+ cfgv.Optional('fail_fast', cfgv.check_bool, False),
cfgv.WarnAdditionalKeys(
(
'repos',
diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py
index 7ed6e776..aa0c5e25 100644
--- a/pre_commit/commands/autoupdate.py
+++ b/pre_commit/commands/autoupdate.py
@@ -1,22 +1,23 @@
from __future__ import annotations
+import concurrent.futures
import os.path
import re
import tempfile
+from collections.abc import Sequence
from typing import Any
from typing import NamedTuple
-from typing import Sequence
import pre_commit.constants as C
from pre_commit import git
from pre_commit import output
+from pre_commit import xargs
from pre_commit.clientlib import InvalidManifestError
from pre_commit.clientlib import load_config
from pre_commit.clientlib import load_manifest
from pre_commit.clientlib import LOCAL
from pre_commit.clientlib import META
from pre_commit.commands.migrate_config import migrate_config
-from pre_commit.store import Store
from pre_commit.util import CalledProcessError
from pre_commit.util import cmd_output
from pre_commit.util import cmd_output_b
@@ -27,49 +28,58 @@ from pre_commit.yaml import yaml_load
class RevInfo(NamedTuple):
repo: str
rev: str
- frozen: str | None
+ frozen: str | None = None
+ hook_ids: frozenset[str] = frozenset()
@classmethod
def from_config(cls, config: dict[str, Any]) -> RevInfo:
- return cls(config['repo'], config['rev'], None)
+ return cls(config['repo'], config['rev'])
def update(self, tags_only: bool, freeze: bool) -> RevInfo:
- git_cmd = ('git', *git.NO_FS_MONITOR)
-
- if tags_only:
- tag_cmd = (
- *git_cmd, 'describe',
- 'FETCH_HEAD', '--tags', '--abbrev=0',
- )
- else:
- tag_cmd = (
- *git_cmd, 'describe',
- 'FETCH_HEAD', '--tags', '--exact',
- )
-
with tempfile.TemporaryDirectory() as tmp:
+ _git = ('git', *git.NO_FS_MONITOR, '-C', tmp)
+
+ if tags_only:
+ tag_opt = '--abbrev=0'
+ else:
+ tag_opt = '--exact'
+ tag_cmd = (*_git, 'describe', 'FETCH_HEAD', '--tags', tag_opt)
+
git.init_repo(tmp, self.repo)
+ cmd_output_b(*_git, 'config', 'extensions.partialClone', 'true')
cmd_output_b(
- *git_cmd, 'fetch', 'origin', 'HEAD', '--tags',
- cwd=tmp,
+ *_git, 'fetch', 'origin', 'HEAD',
+ '--quiet', '--filter=blob:none', '--tags',
)
try:
- rev = cmd_output(*tag_cmd, cwd=tmp)[1].strip()
+ rev = cmd_output(*tag_cmd)[1].strip()
except CalledProcessError:
- cmd = (*git_cmd, 'rev-parse', 'FETCH_HEAD')
- rev = cmd_output(*cmd, cwd=tmp)[1].strip()
+ rev = cmd_output(*_git, 'rev-parse', 'FETCH_HEAD')[1].strip()
else:
if tags_only:
rev = git.get_best_candidate_tag(rev, tmp)
frozen = None
if freeze:
- exact_rev_cmd = (*git_cmd, 'rev-parse', rev)
- exact = cmd_output(*exact_rev_cmd, cwd=tmp)[1].strip()
+ exact = cmd_output(*_git, 'rev-parse', rev)[1].strip()
if exact != rev:
rev, frozen = exact, rev
- return self._replace(rev=rev, frozen=frozen)
+
+ try:
+ # workaround for windows -- see #2865
+ cmd_output_b(*_git, 'show', f'{rev}:{C.MANIFEST_FILE}')
+ cmd_output(*_git, 'checkout', rev, '--', C.MANIFEST_FILE)
+ except CalledProcessError:
+ pass # this will be caught by manifest validating code
+ try:
+ manifest = load_manifest(os.path.join(tmp, C.MANIFEST_FILE))
+ except InvalidManifestError as e:
+ raise RepositoryCannotBeUpdatedError(f'[{self.repo}] {e}')
+ else:
+ hook_ids = frozenset(hook['id'] for hook in manifest)
+
+ return self._replace(rev=rev, frozen=frozen, hook_ids=hook_ids)
class RepositoryCannotBeUpdatedError(RuntimeError):
@@ -79,24 +89,30 @@ class RepositoryCannotBeUpdatedError(RuntimeError):
def _check_hooks_still_exist_at_rev(
repo_config: dict[str, Any],
info: RevInfo,
- store: Store,
) -> None:
- try:
- path = store.clone(repo_config['repo'], info.rev)
- manifest = load_manifest(os.path.join(path, C.MANIFEST_FILE))
- except InvalidManifestError as e:
- raise RepositoryCannotBeUpdatedError(str(e))
-
# See if any of our hooks were deleted with the new commits
hooks = {hook['id'] for hook in repo_config['hooks']}
- hooks_missing = hooks - {hook['id'] for hook in manifest}
+ hooks_missing = hooks - info.hook_ids
if hooks_missing:
raise RepositoryCannotBeUpdatedError(
- f'Cannot update because the update target is missing these '
- f'hooks:\n{", ".join(sorted(hooks_missing))}',
+ f'[{info.repo}] Cannot update because the update target is '
+ f'missing these hooks: {", ".join(sorted(hooks_missing))}',
)
+def _update_one(
+ i: int,
+ repo: dict[str, Any],
+ *,
+ tags_only: bool,
+ freeze: bool,
+) -> tuple[int, RevInfo, RevInfo]:
+ old = RevInfo.from_config(repo)
+ new = old.update(tags_only=tags_only, freeze=freeze)
+ _check_hooks_still_exist_at_rev(repo, new)
+ return i, old, new
+
+
REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([\'"]?)([^\s#]+)(.*)(\r?\n)$')
@@ -145,49 +161,53 @@ def _write_new_config(path: str, rev_infos: list[RevInfo | None]) -> None:
def autoupdate(
config_file: str,
- store: Store,
tags_only: bool,
freeze: bool,
repos: Sequence[str] = (),
+ jobs: int = 1,
) -> int:
"""Auto-update the pre-commit config to the latest versions of repos."""
migrate_config(config_file, quiet=True)
- retv = 0
- rev_infos: list[RevInfo | None] = []
changed = False
+ retv = 0
- config = load_config(config_file)
- for repo_config in config['repos']:
- if repo_config['repo'] in {LOCAL, META}:
- continue
+ config_repos = [
+ repo for repo in load_config(config_file)['repos']
+ if repo['repo'] not in {LOCAL, META}
+ ]
- info = RevInfo.from_config(repo_config)
- if repos and info.repo not in repos:
- rev_infos.append(None)
- continue
-
- output.write(f'Updating {info.repo} ... ')
- new_info = info.update(tags_only=tags_only, freeze=freeze)
- try:
- _check_hooks_still_exist_at_rev(repo_config, new_info, store)
- except RepositoryCannotBeUpdatedError as error:
- output.write_line(error.args[0])
- rev_infos.append(None)
- retv = 1
- continue
-
- if new_info.rev != info.rev:
- changed = True
- if new_info.frozen:
- updated_to = f'{new_info.frozen} (frozen)'
+ rev_infos: list[RevInfo | None] = [None] * len(config_repos)
+ jobs = jobs or xargs.cpu_count() # 0 => number of cpus
+ jobs = min(jobs, len(repos) or len(config_repos)) # max 1-per-thread
+ jobs = max(jobs, 1) # at least one thread
+ with concurrent.futures.ThreadPoolExecutor(jobs) as exe:
+ futures = [
+ exe.submit(
+ _update_one,
+ i, repo, tags_only=tags_only, freeze=freeze,
+ )
+ for i, repo in enumerate(config_repos)
+ if not repos or repo['repo'] in repos
+ ]
+ for future in concurrent.futures.as_completed(futures):
+ try:
+ i, old, new = future.result()
+ except RepositoryCannotBeUpdatedError as e:
+ output.write_line(str(e))
+ retv = 1
else:
- updated_to = new_info.rev
- msg = f'updating {info.rev} -> {updated_to}.'
- output.write_line(msg)
- rev_infos.append(new_info)
- else:
- output.write_line('already up to date.')
- rev_infos.append(None)
+ if new.rev != old.rev:
+ changed = True
+ if new.frozen:
+ new_s = f'{new.frozen} (frozen)'
+ else:
+ new_s = new.rev
+ msg = f'updating {old.rev} -> {new_s}'
+ rev_infos[i] = new
+ else:
+ msg = 'already up to date!'
+
+ output.write_line(f'[{old.repo}] {msg}')
if changed:
_write_new_config(config_file, rev_infos)
diff --git a/pre_commit/commands/gc.py b/pre_commit/commands/gc.py
index 6892e097..975d5e4c 100644
--- a/pre_commit/commands/gc.py
+++ b/pre_commit/commands/gc.py
@@ -12,6 +12,7 @@ from pre_commit.clientlib import load_manifest
from pre_commit.clientlib import LOCAL
from pre_commit.clientlib import META
from pre_commit.store import Store
+from pre_commit.util import rmtree
def _mark_used_repos(
@@ -26,7 +27,8 @@ def _mark_used_repos(
for hook in repo['hooks']:
deps = hook.get('additional_dependencies')
unused_repos.discard((
- store.db_repo_name(repo['repo'], deps), C.LOCAL_REPO_VERSION,
+ store.db_repo_name(repo['repo'], deps),
+ C.LOCAL_REPO_VERSION,
))
else:
key = (repo['repo'], repo['rev'])
@@ -56,34 +58,41 @@ def _mark_used_repos(
))
-def _gc_repos(store: Store) -> int:
- configs = store.select_all_configs()
- repos = store.select_all_repos()
+def _gc(store: Store) -> int:
+ with store.exclusive_lock(), store.connect() as db:
+ store._create_configs_table(db)
- # delete config paths which do not exist
- dead_configs = [p for p in configs if not os.path.exists(p)]
- live_configs = [p for p in configs if os.path.exists(p)]
+ repos = db.execute('SELECT repo, ref, path FROM repos').fetchall()
+ all_repos = {(repo, ref): path for repo, ref, path in repos}
+ unused_repos = set(all_repos)
- all_repos = {(repo, ref): path for repo, ref, path in repos}
- unused_repos = set(all_repos)
- for config_path in live_configs:
- try:
- config = load_config(config_path)
- except InvalidConfigError:
- dead_configs.append(config_path)
- continue
- else:
- for repo in config['repos']:
- _mark_used_repos(store, all_repos, unused_repos, repo)
+ configs_rows = db.execute('SELECT path FROM configs').fetchall()
+ configs = [path for path, in configs_rows]
- store.delete_configs(dead_configs)
- for db_repo_name, ref in unused_repos:
- store.delete_repo(db_repo_name, ref, all_repos[(db_repo_name, ref)])
- return len(unused_repos)
+ dead_configs = []
+ for config_path in configs:
+ try:
+ config = load_config(config_path)
+ except InvalidConfigError:
+ dead_configs.append(config_path)
+ continue
+ else:
+ for repo in config['repos']:
+ _mark_used_repos(store, all_repos, unused_repos, repo)
+
+ paths = [(path,) for path in dead_configs]
+ db.executemany('DELETE FROM configs WHERE path = ?', paths)
+
+ db.executemany(
+ 'DELETE FROM repos WHERE repo = ? and ref = ?',
+ sorted(unused_repos),
+ )
+ for k in unused_repos:
+ rmtree(all_repos[k])
+
+ return len(unused_repos)
def gc(store: Store) -> int:
- with store.exclusive_lock():
- repos_removed = _gc_repos(store)
- output.write_line(f'{repos_removed} repo(s) removed.')
+ output.write_line(f'{_gc(store)} repo(s) removed.')
return 0
diff --git a/pre_commit/commands/hazmat.py b/pre_commit/commands/hazmat.py
new file mode 100644
index 00000000..01b27ce6
--- /dev/null
+++ b/pre_commit/commands/hazmat.py
@@ -0,0 +1,95 @@
+from __future__ import annotations
+
+import argparse
+import subprocess
+from collections.abc import Sequence
+
+from pre_commit.parse_shebang import normalize_cmd
+
+
+def add_parsers(parser: argparse.ArgumentParser) -> None:
+ subparsers = parser.add_subparsers(dest='tool')
+
+ cd_parser = subparsers.add_parser(
+ 'cd', help='cd to a subdir and run the command',
+ )
+ cd_parser.add_argument('subdir')
+ cd_parser.add_argument('cmd', nargs=argparse.REMAINDER)
+
+ ignore_exit_code_parser = subparsers.add_parser(
+ 'ignore-exit-code', help='run the command but ignore the exit code',
+ )
+ ignore_exit_code_parser.add_argument('cmd', nargs=argparse.REMAINDER)
+
+ n1_parser = subparsers.add_parser(
+ 'n1', help='run the command once per filename',
+ )
+ n1_parser.add_argument('cmd', nargs=argparse.REMAINDER)
+
+
+def _cmd_filenames(cmd: tuple[str, ...]) -> tuple[
+ tuple[str, ...],
+ tuple[str, ...],
+]:
+ for idx, val in enumerate(reversed(cmd)):
+ if val == '--':
+ split = len(cmd) - idx
+ break
+ else:
+ raise SystemExit('hazmat entry must end with `--`')
+
+ return cmd[:split - 1], cmd[split:]
+
+
+def cd(subdir: str, cmd: tuple[str, ...]) -> int:
+ cmd, filenames = _cmd_filenames(cmd)
+
+ prefix = f'{subdir}/'
+ new_filenames = []
+ for filename in filenames:
+ if not filename.startswith(prefix):
+ raise SystemExit(f'unexpected file without {prefix=}: {filename}')
+ else:
+ new_filenames.append(filename.removeprefix(prefix))
+
+ cmd = normalize_cmd(cmd)
+ return subprocess.call((*cmd, *new_filenames), cwd=subdir)
+
+
+def ignore_exit_code(cmd: tuple[str, ...]) -> int:
+ cmd = normalize_cmd(cmd)
+ subprocess.call(cmd)
+ return 0
+
+
+def n1(cmd: tuple[str, ...]) -> int:
+ cmd, filenames = _cmd_filenames(cmd)
+ cmd = normalize_cmd(cmd)
+ ret = 0
+ for filename in filenames:
+ ret |= subprocess.call((*cmd, filename))
+ return ret
+
+
+def impl(args: argparse.Namespace) -> int:
+ args.cmd = tuple(args.cmd)
+ if args.tool == 'cd':
+ return cd(args.subdir, args.cmd)
+ elif args.tool == 'ignore-exit-code':
+ return ignore_exit_code(args.cmd)
+ elif args.tool == 'n1':
+ return n1(args.cmd)
+ else:
+ raise NotImplementedError(f'unexpected tool: {args.tool}')
+
+
+def main(argv: Sequence[str] | None = None) -> int:
+ parser = argparse.ArgumentParser()
+ add_parsers(parser)
+ args = parser.parse_args(argv)
+
+ return impl(args)
+
+
+if __name__ == '__main__':
+ raise SystemExit(main())
diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py
index f5995e9a..de5c8f34 100644
--- a/pre_commit/commands/hook_impl.py
+++ b/pre_commit/commands/hook_impl.py
@@ -4,7 +4,7 @@ import argparse
import os.path
import subprocess
import sys
-from typing import Sequence
+from collections.abc import Sequence
from pre_commit.commands.run import run
from pre_commit.envcontext import envcontext
@@ -73,6 +73,8 @@ def _ns(
local_branch: str | None = None,
from_ref: str | None = None,
to_ref: str | None = None,
+ pre_rebase_upstream: str | None = None,
+ pre_rebase_branch: str | None = None,
remote_name: str | None = None,
remote_url: str | None = None,
commit_msg_filename: str | None = None,
@@ -84,11 +86,13 @@ def _ns(
) -> argparse.Namespace:
return argparse.Namespace(
color=color,
- hook_stage=hook_type.replace('pre-', ''),
+ hook_stage=hook_type,
remote_branch=remote_branch,
local_branch=local_branch,
from_ref=from_ref,
to_ref=to_ref,
+ pre_rebase_upstream=pre_rebase_upstream,
+ pre_rebase_branch=pre_rebase_branch,
remote_name=remote_name,
remote_url=remote_url,
commit_msg_filename=commit_msg_filename,
@@ -102,6 +106,7 @@ def _ns(
hook=None,
verbose=False,
show_diff_on_failure=False,
+ fail_fast=False,
)
@@ -185,6 +190,12 @@ def _check_args_length(hook_type: str, args: Sequence[str]) -> None:
f'hook-impl for {hook_type} expected 1, 2, or 3 arguments '
f'but got {len(args)}: {args}',
)
+ elif hook_type == 'pre-rebase':
+ if len(args) < 1 or len(args) > 2:
+ raise SystemExit(
+ f'hook-impl for {hook_type} expected 1 or 2 arguments '
+ f'but got {len(args)}: {args}',
+ )
elif hook_type in _EXPECTED_ARG_LENGTH_BY_HOOK:
expected = _EXPECTED_ARG_LENGTH_BY_HOOK[hook_type]
if len(args) != expected:
@@ -231,6 +242,13 @@ def _run_ns(
return _ns(hook_type, color, is_squash_merge=args[0])
elif hook_type == 'post-rewrite':
return _ns(hook_type, color, rewrite_command=args[0])
+ elif hook_type == 'pre-rebase' and len(args) == 1:
+ return _ns(hook_type, color, pre_rebase_upstream=args[0])
+ elif hook_type == 'pre-rebase' and len(args) == 2:
+ return _ns(
+ hook_type, color, pre_rebase_upstream=args[0],
+ pre_rebase_branch=args[1],
+ )
else:
raise AssertionError(f'unexpected hook type: {hook_type}')
diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py
index 5ff6cba6..d19e0d47 100644
--- a/pre_commit/commands/install_uninstall.py
+++ b/pre_commit/commands/install_uninstall.py
@@ -103,8 +103,7 @@ def _install_hook_script(
hook_file.write(before + TEMPLATE_START)
hook_file.write(f'INSTALL_PYTHON={shlex.quote(sys.executable)}\n')
- # TODO: python3.8+: shlex.join
- args_s = ' '.join(shlex.quote(part) for part in args)
+ args_s = shlex.join(args)
hook_file.write(f'ARGS=({args_s})\n')
hook_file.write(TEMPLATE_END + after)
make_executable(hook_path)
diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py
index 6f7af4eb..b04c53a5 100644
--- a/pre_commit/commands/migrate_config.py
+++ b/pre_commit/commands/migrate_config.py
@@ -1,13 +1,21 @@
from __future__ import annotations
-import re
+import functools
+import itertools
import textwrap
+from collections.abc import Callable
import cfgv
import yaml
+from yaml.nodes import ScalarNode
from pre_commit.clientlib import InvalidConfigError
+from pre_commit.yaml import yaml_compose
from pre_commit.yaml import yaml_load
+from pre_commit.yaml_rewrite import MappingKey
+from pre_commit.yaml_rewrite import MappingValue
+from pre_commit.yaml_rewrite import match
+from pre_commit.yaml_rewrite import SequenceItem
def _is_header_line(line: str) -> bool:
@@ -38,8 +46,69 @@ def _migrate_map(contents: str) -> str:
return contents
-def _migrate_sha_to_rev(contents: str) -> str:
- return re.sub(r'(\n\s+)sha:', r'\1rev:', contents)
+def _preserve_style(n: ScalarNode, *, s: str) -> str:
+ style = n.style or ''
+ return f'{style}{s}{style}'
+
+
+def _fix_stage(n: ScalarNode) -> str:
+ return _preserve_style(n, s=f'pre-{n.value}')
+
+
+def _migrate_composed(contents: str) -> str:
+ tree = yaml_compose(contents)
+ rewrites: list[tuple[ScalarNode, Callable[[ScalarNode], str]]] = []
+
+ # sha -> rev
+ sha_to_rev_replace = functools.partial(_preserve_style, s='rev')
+ sha_to_rev_matcher = (
+ MappingValue('repos'),
+ SequenceItem(),
+ MappingKey('sha'),
+ )
+ for node in match(tree, sha_to_rev_matcher):
+ rewrites.append((node, sha_to_rev_replace))
+
+ # python_venv -> python
+ language_matcher = (
+ MappingValue('repos'),
+ SequenceItem(),
+ MappingValue('hooks'),
+ SequenceItem(),
+ MappingValue('language'),
+ )
+ python_venv_replace = functools.partial(_preserve_style, s='python')
+ for node in match(tree, language_matcher):
+ if node.value == 'python_venv':
+ rewrites.append((node, python_venv_replace))
+
+ # stages rewrites
+ default_stages_matcher = (MappingValue('default_stages'), SequenceItem())
+ default_stages_match = match(tree, default_stages_matcher)
+ hook_stages_matcher = (
+ MappingValue('repos'),
+ SequenceItem(),
+ MappingValue('hooks'),
+ SequenceItem(),
+ MappingValue('stages'),
+ SequenceItem(),
+ )
+ hook_stages_match = match(tree, hook_stages_matcher)
+ for node in itertools.chain(default_stages_match, hook_stages_match):
+ if node.value in {'commit', 'push', 'merge-commit'}:
+ rewrites.append((node, _fix_stage))
+
+ rewrites.sort(reverse=True, key=lambda nf: nf[0].start_mark.index)
+
+ src_parts = []
+ end: int | None = None
+ for node, func in rewrites:
+ src_parts.append(contents[node.end_mark.index:end])
+ src_parts.append(func(node))
+ end = node.start_mark.index
+ src_parts.append(contents[:end])
+ src_parts.reverse()
+ return ''.join(src_parts)
def migrate_config(config_file: str, quiet: bool = False) -> int:
@@ -54,7 +123,7 @@ def migrate_config(config_file: str, quiet: bool = False) -> int:
raise cfgv.ValidationError(str(e))
contents = _migrate_map(contents)
- contents = _migrate_sha_to_rev(contents)
+ contents = _migrate_composed(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 e44e7036..8ab505ff 100644
--- a/pre_commit/commands/run.py
+++ b/pre_commit/commands/run.py
@@ -9,19 +9,20 @@ import re
import subprocess
import time
import unicodedata
+from collections.abc import Generator
+from collections.abc import Iterable
+from collections.abc import MutableMapping
+from collections.abc import Sequence
from typing import Any
-from typing import Collection
-from typing import MutableMapping
-from typing import Sequence
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
@@ -57,37 +58,36 @@ def _full_msg(
def filter_by_include_exclude(
- names: Collection[str],
+ names: Iterable[str],
include: str,
exclude: str,
-) -> list[str]:
+) -> Generator[str]:
include_re, exclude_re = re.compile(include), re.compile(exclude)
- return [
+ return (
filename for filename in names
if include_re.search(filename)
if not exclude_re.search(filename)
- ]
+ )
class Classifier:
- def __init__(self, filenames: Collection[str]) -> None:
+ def __init__(self, filenames: Iterable[str]) -> None:
self.filenames = [f for f in filenames if os.path.lexists(f)]
- @functools.lru_cache(maxsize=None)
+ @functools.cache
def _types_for_file(self, filename: str) -> set[str]:
return tags_from_path(filename)
def by_types(
self,
- names: Sequence[str],
- types: Collection[str],
- types_or: Collection[str],
- exclude_types: Collection[str],
- ) -> list[str]:
+ names: Iterable[str],
+ types: Iterable[str],
+ types_or: Iterable[str],
+ exclude_types: Iterable[str],
+ ) -> Generator[str]:
types = frozenset(types)
types_or = frozenset(types_or)
exclude_types = frozenset(exclude_types)
- ret = []
for filename in names:
tags = self._types_for_file(filename)
if (
@@ -95,24 +95,24 @@ class Classifier:
(not types_or or tags & types_or) and
not tags & exclude_types
):
- ret.append(filename)
- return ret
+ yield filename
- def filenames_for_hook(self, hook: Hook) -> tuple[str, ...]:
- names = self.filenames
- names = filter_by_include_exclude(names, hook.files, hook.exclude)
- names = self.by_types(
- names,
+ def filenames_for_hook(self, hook: Hook) -> Generator[str]:
+ return self.by_types(
+ filter_by_include_exclude(
+ self.filenames,
+ hook.files,
+ hook.exclude,
+ ),
hook.types,
hook.types_or,
hook.exclude_types,
)
- return tuple(names)
@classmethod
def from_config(
cls,
- filenames: Collection[str],
+ filenames: Iterable[str],
include: str,
exclude: str,
) -> Classifier:
@@ -121,7 +121,7 @@ class Classifier:
# this also makes improperly quoted shell-based hooks work better
# see #1173
if os.altsep == '/' and os.sep == '\\':
- filenames = [f.replace(os.sep, os.altsep) for f in filenames]
+ filenames = (f.replace(os.sep, os.altsep) for f in filenames)
filenames = filter_by_include_exclude(filenames, include, exclude)
return Classifier(filenames)
@@ -148,7 +148,7 @@ def _run_single_hook(
verbose: bool,
use_color: bool,
) -> tuple[bool, bytes]:
- filenames = classifier.filenames_for_hook(hook)
+ filenames = tuple(classifier.filenames_for_hook(hook))
if hook.id in skips or hook.alias in skips:
output.write(
@@ -187,7 +187,7 @@ def _run_single_hook(
if not hook.pass_filenames:
filenames = ()
- time_before = time.time()
+ time_before = time.monotonic()
language = languages[hook.language]
with language.in_env(hook.prefix, hook.language_version):
retcode, out = language.run_hook(
@@ -199,7 +199,7 @@ def _run_single_hook(
require_serial=hook.require_serial,
color=use_color,
)
- duration = round(time.time() - time_before, 2) or 0
+ duration = round(time.monotonic() - time_before, 2) or 0
diff_after = _get_diff()
# if the hook makes changes, fail the commit
@@ -250,10 +250,11 @@ def _compute_cols(hooks: Sequence[Hook]) -> int:
return max(cols, 80)
-def _all_filenames(args: argparse.Namespace) -> Collection[str]:
+def _all_filenames(args: argparse.Namespace) -> Iterable[str]:
# these hooks do not operate on files
if args.hook_stage in {
'post-checkout', 'post-commit', 'post-merge', 'post-rewrite',
+ 'pre-rebase',
}:
return ()
elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}:
@@ -272,7 +273,8 @@ def _all_filenames(args: argparse.Namespace) -> Collection[str]:
def _get_diff() -> bytes:
_, out, _ = cmd_output_b(
- 'git', 'diff', '--no-ext-diff', '--ignore-submodules', check=False,
+ 'git', 'diff', '--no-ext-diff', '--no-textconv', '--ignore-submodules',
+ check=False,
)
return out
@@ -296,7 +298,8 @@ def _run_hooks(
verbose=args.verbose, use_color=args.color,
)
retval |= current_retval
- if retval and (config['fail_fast'] or hook.fail_fast):
+ fail_fast = (config['fail_fast'] or hook.fail_fast or args.fail_fast)
+ if current_retval and fail_fast:
break
if retval and args.show_diff_on_failure and prior_diff:
if args.all_files:
@@ -326,8 +329,7 @@ def _has_unmerged_paths() -> bool:
def _has_unstaged_config(config_file: str) -> bool:
retcode, _, _ = cmd_output_b(
- 'git', 'diff', '--no-ext-diff', '--exit-code', config_file,
- check=False,
+ 'git', 'diff', '--quiet', '--no-ext-diff', config_file, check=False,
)
# be explicit, other git errors don't mean it has an unstaged config.
return retcode == 1
@@ -389,6 +391,10 @@ def run(
environ['PRE_COMMIT_FROM_REF'] = args.from_ref
environ['PRE_COMMIT_TO_REF'] = args.to_ref
+ if args.pre_rebase_upstream and args.pre_rebase_branch:
+ environ['PRE_COMMIT_PRE_REBASE_UPSTREAM'] = args.pre_rebase_upstream
+ environ['PRE_COMMIT_PRE_REBASE_BRANCH'] = args.pre_rebase_branch
+
if (
args.remote_name and args.remote_url and
args.remote_branch and args.local_branch
diff --git a/pre_commit/commands/validate_config.py b/pre_commit/commands/validate_config.py
index 24bd3135..b3de635b 100644
--- a/pre_commit/commands/validate_config.py
+++ b/pre_commit/commands/validate_config.py
@@ -1,6 +1,6 @@
from __future__ import annotations
-from typing import Sequence
+from collections.abc import Sequence
from pre_commit import clientlib
diff --git a/pre_commit/commands/validate_manifest.py b/pre_commit/commands/validate_manifest.py
index 419031a9..8493c6e1 100644
--- a/pre_commit/commands/validate_manifest.py
+++ b/pre_commit/commands/validate_manifest.py
@@ -1,6 +1,6 @@
from __future__ import annotations
-from typing import Sequence
+from collections.abc import Sequence
from pre_commit import clientlib
diff --git a/pre_commit/constants.py b/pre_commit/constants.py
index 3f03ceed..79a9bb69 100644
--- a/pre_commit/constants.py
+++ b/pre_commit/constants.py
@@ -10,17 +10,4 @@ LOCAL_REPO_VERSION = '1'
VERSION = importlib.metadata.version('pre_commit')
-# `manual` is not invoked by any installed git hook. See #719
-STAGES = (
- 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg',
- 'post-commit', 'manual', 'post-checkout', 'push', 'post-merge',
- 'post-rewrite',
-)
-
-HOOK_TYPES = (
- 'pre-commit', 'pre-merge-commit', 'pre-push', 'prepare-commit-msg',
- 'commit-msg', 'post-commit', 'post-checkout', 'post-merge',
- 'post-rewrite',
-)
-
DEFAULT = 'default'
diff --git a/pre_commit/envcontext.py b/pre_commit/envcontext.py
index 4f595601..d4d24118 100644
--- a/pre_commit/envcontext.py
+++ b/pre_commit/envcontext.py
@@ -3,10 +3,9 @@ from __future__ import annotations
import contextlib
import enum
import os
-from typing import Generator
-from typing import MutableMapping
+from collections.abc import Generator
+from collections.abc import MutableMapping
from typing import NamedTuple
-from typing import Tuple
from typing import Union
_Unset = enum.Enum('_Unset', 'UNSET')
@@ -18,9 +17,9 @@ class Var(NamedTuple):
default: str = ''
-SubstitutionT = Tuple[Union[str, Var], ...]
+SubstitutionT = tuple[Union[str, Var], ...]
ValueT = Union[str, _Unset, SubstitutionT]
-PatchesT = Tuple[Tuple[str, ValueT], ...]
+PatchesT = tuple[tuple[str, ValueT], ...]
def format_env(parts: SubstitutionT, env: MutableMapping[str, str]) -> str:
@@ -34,7 +33,7 @@ def format_env(parts: SubstitutionT, env: MutableMapping[str, str]) -> str:
def envcontext(
patch: PatchesT,
_env: MutableMapping[str, str] | None = None,
-) -> Generator[None, None, None]:
+) -> Generator[None]:
"""In this context, `os.environ` is modified according to `patch`.
`patch` is an iterable of 2-tuples (key, value):
diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py
index d740ee3e..4f0e0573 100644
--- a/pre_commit/error_handler.py
+++ b/pre_commit/error_handler.py
@@ -5,7 +5,7 @@ import functools
import os.path
import sys
import traceback
-from typing import Generator
+from collections.abc import Generator
from typing import IO
import pre_commit.constants as C
@@ -68,7 +68,7 @@ def _log_and_exit(
@contextlib.contextmanager
-def error_handler() -> Generator[None, None, None]:
+def error_handler() -> Generator[None]:
try:
yield
except (Exception, KeyboardInterrupt) as e:
diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py
index f67a5864..6223f869 100644
--- a/pre_commit/file_lock.py
+++ b/pre_commit/file_lock.py
@@ -3,8 +3,8 @@ from __future__ import annotations
import contextlib
import errno
import sys
-from typing import Callable
-from typing import Generator
+from collections.abc import Callable
+from collections.abc import Generator
if sys.platform == 'win32': # pragma: no cover (windows)
@@ -20,7 +20,7 @@ if sys.platform == 'win32': # pragma: no cover (windows)
def _locked(
fileno: int,
blocked_cb: Callable[[], None],
- ) -> Generator[None, None, None]:
+ ) -> Generator[None]:
try:
msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region)
except OSError:
@@ -53,7 +53,7 @@ else: # pragma: win32 no cover
def _locked(
fileno: int,
blocked_cb: Callable[[], None],
- ) -> Generator[None, None, None]:
+ ) -> Generator[None]:
try:
fcntl.flock(fileno, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError: # pragma: no cover (tests are single-threaded)
@@ -69,7 +69,7 @@ else: # pragma: win32 no cover
def lock(
path: str,
blocked_cb: Callable[[], None],
-) -> Generator[None, None, None]:
+) -> Generator[None]:
with open(path, 'a+') as f:
with _locked(f.fileno(), blocked_cb):
yield
diff --git a/pre_commit/git.py b/pre_commit/git.py
index 333dc7ba..ec1928f3 100644
--- a/pre_commit/git.py
+++ b/pre_commit/git.py
@@ -3,7 +3,7 @@ from __future__ import annotations
import logging
import os.path
import sys
-from typing import Mapping
+from collections.abc import Mapping
from pre_commit.errors import FatalError
from pre_commit.util import CalledProcessError
@@ -126,7 +126,7 @@ def get_conflicted_files() -> set[str]:
merge_diff_filenames = zsplit(
cmd_output(
'git', 'diff', '--name-only', '--no-ext-diff', '-z',
- '-m', tree_hash, 'HEAD', 'MERGE_HEAD',
+ '-m', tree_hash, 'HEAD', 'MERGE_HEAD', '--',
)[1],
)
return set(merge_conflict_filenames) | set(merge_diff_filenames)
@@ -219,7 +219,7 @@ def check_for_cygwin_mismatch() -> None:
if is_cygwin_python ^ is_cygwin_git:
exe_type = {True: '(cygwin)', False: '(windows)'}
- logger.warn(
+ logger.warning(
f'pre-commit has detected a mix of cygwin python / git\n'
f'This combination is not supported, it is likely you will '
f'receive an error later in the program.\n'
diff --git a/pre_commit/hook.py b/pre_commit/hook.py
index 6d436ca3..309cd5be 100644
--- a/pre_commit/hook.py
+++ b/pre_commit/hook.py
@@ -1,9 +1,9 @@
from __future__ import annotations
import logging
+from collections.abc import Sequence
from typing import Any
from typing import NamedTuple
-from typing import Sequence
from pre_commit.prefix import Prefix
diff --git a/pre_commit/languages/helpers.py b/pre_commit/lang_base.py
similarity index 67%
rename from pre_commit/languages/helpers.py
rename to pre_commit/lang_base.py
index d1be409c..198e9365 100644
--- a/pre_commit/languages/helpers.py
+++ b/pre_commit/lang_base.py
@@ -1,27 +1,65 @@
from __future__ import annotations
import contextlib
-import multiprocessing
import os
import random
import re
import shlex
+import sys
+from collections.abc import Generator
+from collections.abc import Sequence
from typing import Any
-from typing import Generator
+from typing import ContextManager
from typing import NoReturn
-from typing import Sequence
+from typing import Protocol
import pre_commit.constants as C
from pre_commit import parse_shebang
+from pre_commit import xargs
from pre_commit.prefix import Prefix
from pre_commit.util import cmd_output_b
-from pre_commit.xargs import xargs
FIXED_RANDOM_SEED = 1542676187
SHIMS_RE = re.compile(r'[/\\]shims[/\\]')
+class Language(Protocol):
+ # Use `None` for no installation / environment
+ @property
+ def ENVIRONMENT_DIR(self) -> str | None: ...
+ # return a value to replace `'default` for `language_version`
+ def get_default_version(self) -> str: ...
+ # return whether the environment is healthy (or should be rebuilt)
+ def health_check(self, prefix: Prefix, version: str) -> str | None: ...
+
+ # install a repository for the given language and language_version
+ def install_environment(
+ self,
+ prefix: Prefix,
+ version: str,
+ additional_dependencies: Sequence[str],
+ ) -> None:
+ ...
+
+ # modify the environment for hook execution
+ def in_env(self, prefix: Prefix, version: str) -> ContextManager[None]: ...
+
+ # execute a hook and return the exit code and output
+ def run_hook(
+ self,
+ prefix: Prefix,
+ entry: str,
+ args: Sequence[str],
+ file_args: Sequence[str],
+ *,
+ is_local: bool,
+ require_serial: bool,
+ color: bool,
+ ) -> tuple[int, bytes]:
+ ...
+
+
def exe_exists(exe: str) -> bool:
found = parse_shebang.find_executable(exe)
if found is None: # exe exists
@@ -45,7 +83,7 @@ def exe_exists(exe: str) -> bool:
)
-def run_setup_cmd(prefix: Prefix, cmd: tuple[str, ...], **kwargs: Any) -> None:
+def setup_cmd(prefix: Prefix, cmd: tuple[str, ...], **kwargs: Any) -> None:
cmd_output_b(*cmd, cwd=prefix.prefix_dir, **kwargs)
@@ -90,7 +128,7 @@ def no_install(
@contextlib.contextmanager
-def no_env(prefix: Prefix, version: str) -> Generator[None, None, None]:
+def no_env(prefix: Prefix, version: str) -> Generator[None]:
yield
@@ -102,10 +140,7 @@ def target_concurrency() -> int:
if 'TRAVIS' in os.environ:
return 2
else:
- try:
- return multiprocessing.cpu_count()
- except NotImplementedError:
- return 1
+ return xargs.cpu_count()
def _shuffled(seq: Sequence[str]) -> list[str]:
@@ -133,11 +168,14 @@ def run_xargs(
# ordering.
file_args = _shuffled(file_args)
jobs = target_concurrency()
- return xargs(cmd, file_args, target_concurrency=jobs, color=color)
+ return xargs.xargs(cmd, file_args, target_concurrency=jobs, color=color)
def hook_cmd(entry: str, args: Sequence[str]) -> tuple[str, ...]:
- return (*shlex.split(entry), *args)
+ cmd = shlex.split(entry)
+ if cmd[:2] == ['pre-commit', 'hazmat']:
+ cmd = [sys.executable, '-m', 'pre_commit.commands.hazmat', *cmd[2:]]
+ return (*cmd, *args)
def basic_run_hook(
diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py
deleted file mode 100644
index d952ae1a..00000000
--- 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 e2fb0196..d397ebeb 100644
--- a/pre_commit/languages/conda.py
+++ b/pre_commit/languages/conda.py
@@ -2,22 +2,23 @@ from __future__ import annotations
import contextlib
import os
-from typing import Generator
-from typing import Sequence
+import sys
+from collections.abc import Generator
+from collections.abc import Sequence
+from pre_commit import lang_base
from pre_commit.envcontext import envcontext
from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import 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:
@@ -26,7 +27,7 @@ def get_env_patch(env: str) -> PatchesT:
# $CONDA_PREFIX/Scripts and $CONDA_PREFIX. Whereas the latter only
# seems to be used for python.exe.
path: SubstitutionT = (os.path.join(env, 'bin'), os.pathsep, Var('PATH'))
- if os.name == 'nt': # pragma: no cover (platform specific)
+ if sys.platform == 'win32': # pragma: win32 cover
path = (env, os.pathsep, *path)
path = (os.path.join(env, 'Scripts'), os.pathsep, *path)
path = (os.path.join(env, 'Library', 'bin'), os.pathsep, *path)
@@ -40,8 +41,8 @@ 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)
+def in_env(prefix: Prefix, version: str) -> Generator[None]:
+ envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir)):
yield
@@ -60,11 +61,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 60757588..08f9a958 100644
--- a/pre_commit/languages/coursier.py
+++ b/pre_commit/languages/coursier.py
@@ -2,22 +2,22 @@ from __future__ import annotations
import contextlib
import os.path
-from typing import Generator
-from typing import Sequence
+from collections.abc import Generator
+from collections.abc import Sequence
+from pre_commit import lang_base
from pre_commit.envcontext import envcontext
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')
@@ -70,7 +70,7 @@ def get_env_patch(target_dir: str) -> PatchesT:
@contextlib.contextmanager
-def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]:
- envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version)
+def in_env(prefix: Prefix, version: str) -> Generator[None]:
+ envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir)):
yield
diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py
index e3c1c585..52a229ee 100644
--- a/pre_commit/languages/dart.py
+++ b/pre_commit/languages/dart.py
@@ -4,22 +4,22 @@ import contextlib
import os.path
import shutil
import tempfile
-from typing import Generator
-from typing import Sequence
+from collections.abc import Generator
+from collections.abc import Sequence
+from pre_commit import lang_base
from pre_commit.envcontext import envcontext
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:
@@ -29,8 +29,8 @@ 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)
+def in_env(prefix: Prefix, version: str) -> Generator[None]:
+ envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir)):
yield
@@ -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 e80c9597..7f45ac86 100644
--- a/pre_commit/languages/docker.py
+++ b/pre_commit/languages/docker.py
@@ -1,46 +1,51 @@
from __future__ import annotations
+import contextlib
+import functools
import hashlib
import json
import os
-from typing import Sequence
+import re
+from collections.abc 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
+
+_HOSTNAME_MOUNT_RE = re.compile(
+ rb"""
+ /containers
+ (?:/overlay-containers)?
+ /([a-z0-9]{64})
+ (?:/userdata)?
+ /hostname
+ """,
+ re.VERBOSE,
+)
-def _is_in_docker() -> bool:
- try:
- with open('/proc/1/cgroup', 'rb') as f:
- return b'docker' in f.read()
- except FileNotFoundError:
- return False
+def _get_container_id() -> str | None:
+ with contextlib.suppress(FileNotFoundError):
+ with open('/proc/1/mountinfo', 'rb') as f:
+ for line in f:
+ m = _HOSTNAME_MOUNT_RE.search(line)
+ if m:
+ return m[1].decode()
-
-def _get_container_id() -> str:
- # It's assumed that we already check /proc/1/cgroup in _is_in_docker. The
- # cpuset cgroup controller existed since cgroups were introduced so this
- # way of getting the container ID is pretty reliable.
- with open('/proc/1/cgroup', 'rb') as f:
- for line in f.readlines():
- if line.split(b':')[1] == b'cpuset':
- return os.path.basename(line.split(b':')[2]).strip().decode()
- raise RuntimeError('Failed to find the container ID in /proc/1/cgroup.')
+ return None
def _get_docker_path(path: str) -> str:
- if not _is_in_docker():
- return path
-
container_id = _get_container_id()
+ if container_id is None:
+ return path
try:
_, out, _ = cmd_output_b('docker', 'inspect', container_id)
@@ -84,16 +89,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
@@ -101,17 +106,47 @@ def install_environment(
os.mkdir(directory)
+@functools.lru_cache(maxsize=1)
+def _is_rootless() -> bool: # pragma: win32 no cover
+ retcode, out, _ = cmd_output_b(
+ 'docker', 'system', 'info', '--format', '{{ json . }}',
+ )
+ if retcode != 0:
+ return False
+
+ info = json.loads(out)
+ try:
+ return (
+ # docker:
+ # https://docs.docker.com/reference/api/engine/version/v1.48/#tag/System/operation/SystemInfo
+ 'name=rootless' in (info.get('SecurityOptions') or ()) or
+ # podman:
+ # https://docs.podman.io/en/latest/_static/api.html?version=v5.4#tag/system/operation/SystemInfoLibpod
+ info['host']['security']['rootless']
+ )
+ except KeyError:
+ return False
+
+
def get_docker_user() -> tuple[str, ...]: # pragma: win32 no cover
+ if _is_rootless():
+ return ()
+
try:
return ('-u', f'{os.getuid()}:{os.getgid()}')
except AttributeError:
return ()
-def docker_cmd() -> tuple[str, ...]: # pragma: win32 no cover
+def get_docker_tty(*, color: bool) -> tuple[str, ...]: # pragma: win32 no cover # noqa: E501
+ return (('--tty',) if color else ())
+
+
+def docker_cmd(*, color: bool) -> tuple[str, ...]: # pragma: win32 no cover
return (
'docker', 'run',
'--rm',
+ *get_docker_tty(color=color),
*get_docker_user(),
# https://docs.docker.com/engine/reference/commandline/run/#mount-volumes-from-container-volumes-from
# The `Z` option tells Docker to label the content with a private
@@ -135,12 +170,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(color=color), *entry_tag, *cmd_rest),
file_args,
require_serial=require_serial,
color=color,
diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py
index 8e5f2c04..60caa101 100644
--- a/pre_commit/languages/docker_image.py
+++ b/pre_commit/languages/docker_image.py
@@ -1,16 +1,16 @@
from __future__ import annotations
-from typing import Sequence
+from collections.abc 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(color=color) + 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 4c3955e8..ffc65d1e 100644
--- a/pre_commit/languages/dotnet.py
+++ b/pre_commit/languages/dotnet.py
@@ -6,21 +6,21 @@ import re
import tempfile
import xml.etree.ElementTree
import zipfile
-from typing import Generator
-from typing import Sequence
+from collections.abc import Generator
+from collections.abc import Sequence
+from pre_commit import lang_base
from pre_commit.envcontext import envcontext
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:
@@ -30,14 +30,14 @@ 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)
+def in_env(prefix: Prefix, version: str) -> Generator[None]:
+ envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir)):
yield
@contextlib.contextmanager
-def _nuget_config_no_sources() -> Generator[str, None, None]:
+def _nuget_config_no_sources() -> Generator[str]:
with tempfile.TemporaryDirectory() as tmpdir:
nuget_config = os.path.join(tmpdir, 'nuget.config')
with open(nuget_config, 'w') as f:
@@ -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 33df067e..6ac4d767 100644
--- a/pre_commit/languages/fail.py
+++ b/pre_commit/languages/fail.py
@@ -1,15 +1,15 @@
from __future__ import annotations
-from typing import Sequence
+from collections.abc 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 3c4b652f..bedbd114 100644
--- a/pre_commit/languages/golang.py
+++ b/pre_commit/languages/golang.py
@@ -12,24 +12,25 @@ import tempfile
import urllib.error
import urllib.request
import zipfile
+from collections.abc import Generator
+from collections.abc import Sequence
from typing import ContextManager
-from typing import Generator
from typing import IO
from typing import Protocol
-from typing import Sequence
import pre_commit.constants as C
+from pre_commit import lang_base
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.git import no_git_env
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 +61,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
@@ -74,6 +75,7 @@ def get_env_patch(venv: str, version: str) -> PatchesT:
return (
('GOROOT', os.path.join(venv, '.go')),
+ ('GOTOOLCHAIN', 'local'),
(
'PATH', (
os.path.join(venv, 'bin'), os.pathsep,
@@ -88,8 +90,7 @@ def _infer_go_version(version: str) -> str:
if version != C.DEFAULT:
return version
resp = urllib.request.urlopen('https://go.dev/dl/?mode=json')
- # TODO: 3.9+ .removeprefix('go')
- return json.load(resp)[0]['version'][2:]
+ return json.load(resp)[0]['version'].removeprefix('go')
def _get_url(version: str) -> str:
@@ -120,8 +121,8 @@ 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)
+def in_env(prefix: Prefix, version: str) -> Generator[None]:
+ envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir, version)):
yield
@@ -131,7 +132,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)
@@ -141,17 +142,18 @@ def install_environment(
else:
gopath = env_dir
- env = dict(os.environ, GOPATH=gopath)
+ env = no_git_env(dict(os.environ, GOPATH=gopath))
env.pop('GOBIN', None)
if version != 'system':
+ env['GOTOOLCHAIN'] = 'local'
env['GOROOT'] = os.path.join(env_dir, '.go')
env['PATH'] = os.pathsep.join((
os.path.join(env_dir, '.go', 'bin'), os.environ['PATH'],
))
- 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/haskell.py b/pre_commit/languages/haskell.py
new file mode 100644
index 00000000..28bca08c
--- /dev/null
+++ b/pre_commit/languages/haskell.py
@@ -0,0 +1,56 @@
+from __future__ import annotations
+
+import contextlib
+import os.path
+from collections.abc import Generator
+from collections.abc import Sequence
+
+from pre_commit import lang_base
+from pre_commit.envcontext import envcontext
+from pre_commit.envcontext import PatchesT
+from pre_commit.envcontext import Var
+from pre_commit.errors import FatalError
+from pre_commit.prefix import Prefix
+
+ENVIRONMENT_DIR = 'hs_env'
+get_default_version = lang_base.basic_get_default_version
+health_check = lang_base.basic_health_check
+run_hook = lang_base.basic_run_hook
+
+
+def get_env_patch(target_dir: str) -> PatchesT:
+ bin_path = os.path.join(target_dir, 'bin')
+ return (('PATH', (bin_path, os.pathsep, Var('PATH'))),)
+
+
+@contextlib.contextmanager
+def in_env(prefix: Prefix, version: str) -> Generator[None]:
+ envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
+ with envcontext(get_env_patch(envdir)):
+ yield
+
+
+def install_environment(
+ prefix: Prefix,
+ version: str,
+ additional_dependencies: Sequence[str],
+) -> None:
+ lang_base.assert_version_default('haskell', version)
+ envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
+
+ pkgs = [*prefix.star('.cabal'), *additional_dependencies]
+ if not pkgs:
+ raise FatalError('Expected .cabal files or additional_dependencies')
+
+ bindir = os.path.join(envdir, 'bin')
+ os.makedirs(bindir, exist_ok=True)
+ lang_base.setup_cmd(prefix, ('cabal', 'update'))
+ lang_base.setup_cmd(
+ prefix,
+ (
+ 'cabal', 'install',
+ '--install-method', 'copy',
+ '--installdir', bindir,
+ *pkgs,
+ ),
+ )
diff --git a/pre_commit/languages/julia.py b/pre_commit/languages/julia.py
new file mode 100644
index 00000000..7559b5ba
--- /dev/null
+++ b/pre_commit/languages/julia.py
@@ -0,0 +1,133 @@
+from __future__ import annotations
+
+import contextlib
+import os
+import shutil
+from collections.abc import Generator
+from collections.abc import Sequence
+
+from pre_commit import lang_base
+from pre_commit.envcontext import envcontext
+from pre_commit.envcontext import PatchesT
+from pre_commit.envcontext import UNSET
+from pre_commit.prefix import Prefix
+from pre_commit.util import cmd_output_b
+
+ENVIRONMENT_DIR = 'juliaenv'
+health_check = lang_base.basic_health_check
+get_default_version = lang_base.basic_get_default_version
+
+
+def run_hook(
+ prefix: Prefix,
+ entry: str,
+ args: Sequence[str],
+ file_args: Sequence[str],
+ *,
+ is_local: bool,
+ require_serial: bool,
+ color: bool,
+) -> tuple[int, bytes]:
+ # `entry` is a (hook-repo relative) file followed by (optional) args, e.g.
+ # `bin/id.jl` or `bin/hook.jl --arg1 --arg2` so we
+ # 1) shell parse it and join with args with hook_cmd
+ # 2) prepend the hooks prefix path to the first argument (the file), unless
+ # it is a local script
+ # 3) prepend `julia` as the interpreter
+
+ cmd = lang_base.hook_cmd(entry, args)
+ script = cmd[0] if is_local else prefix.path(cmd[0])
+ cmd = ('julia', '--startup-file=no', script, *cmd[1:])
+ return lang_base.run_xargs(
+ cmd,
+ file_args,
+ require_serial=require_serial,
+ color=color,
+ )
+
+
+def get_env_patch(target_dir: str, version: str) -> PatchesT:
+ return (
+ ('JULIA_LOAD_PATH', target_dir),
+ # May be set, remove it to not interfer with LOAD_PATH
+ ('JULIA_PROJECT', UNSET),
+ )
+
+
+@contextlib.contextmanager
+def in_env(prefix: Prefix, version: str) -> Generator[None]:
+ envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
+ with envcontext(get_env_patch(envdir, version)):
+ yield
+
+
+def install_environment(
+ prefix: Prefix,
+ version: str,
+ additional_dependencies: Sequence[str],
+) -> None:
+ envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
+ with in_env(prefix, version):
+ # TODO: Support language_version with juliaup similar to rust via
+ # rustup
+ # if version != 'system':
+ # ...
+
+ # Copy Project.toml to hook env if it exist
+ os.makedirs(envdir, exist_ok=True)
+ project_names = ('JuliaProject.toml', 'Project.toml')
+ project_found = False
+ for project_name in project_names:
+ project_file = prefix.path(project_name)
+ if not os.path.isfile(project_file):
+ continue
+ shutil.copy(project_file, envdir)
+ project_found = True
+ break
+
+ # If no project file was found we create an empty one so that the
+ # package manager doesn't error
+ if not project_found:
+ open(os.path.join(envdir, 'Project.toml'), 'a').close()
+
+ # Copy Manifest.toml to hook env if it exists
+ manifest_names = ('JuliaManifest.toml', 'Manifest.toml')
+ for manifest_name in manifest_names:
+ manifest_file = prefix.path(manifest_name)
+ if not os.path.isfile(manifest_file):
+ continue
+ shutil.copy(manifest_file, envdir)
+ break
+
+ # Julia code to instantiate the hook environment
+ julia_code = """
+ @assert length(ARGS) > 0
+ hook_env = ARGS[1]
+ deps = join(ARGS[2:end], " ")
+
+ # We prepend @stdlib here so that we can load the package manager even
+ # though `get_env_patch` limits `JULIA_LOAD_PATH` to just the hook env.
+ pushfirst!(LOAD_PATH, "@stdlib")
+ using Pkg
+ popfirst!(LOAD_PATH)
+
+ # Instantiate the environment shipped with the hook repo. If we have
+ # additional dependencies we disable precompilation in this step to
+ # avoid double work.
+ precompile = isempty(deps) ? "1" : "0"
+ withenv("JULIA_PKG_PRECOMPILE_AUTO" => precompile) do
+ Pkg.instantiate()
+ end
+
+ # Add additional dependencies (with precompilation)
+ if !isempty(deps)
+ withenv("JULIA_PKG_PRECOMPILE_AUTO" => "1") do
+ Pkg.REPLMode.pkgstr("add " * deps)
+ end
+ end
+ """
+ cmd_output_b(
+ 'julia', '--startup-file=no', '-e', julia_code, '--', envdir,
+ *additional_dependencies,
+ cwd=prefix.prefix_dir,
+ )
diff --git a/pre_commit/languages/lua.py b/pre_commit/languages/lua.py
index ffc40b50..15ac1a2e 100644
--- a/pre_commit/languages/lua.py
+++ b/pre_commit/languages/lua.py
@@ -3,20 +3,20 @@ from __future__ import annotations
import contextlib
import os
import sys
-from typing import Generator
-from typing import Sequence
+from collections.abc import Generator
+from collections.abc import Sequence
+from pre_commit import lang_base
from pre_commit.envcontext import envcontext
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
@@ -44,8 +44,8 @@ def get_env_patch(d: str) -> PatchesT: # pragma: win32 no cover
@contextlib.contextmanager # pragma: win32 no cover
-def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]:
- envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version)
+def in_env(prefix: Prefix, version: str) -> Generator[None]:
+ envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir)):
yield
@@ -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 9688da35..af7dc6f8 100644
--- a/pre_commit/languages/node.py
+++ b/pre_commit/languages/node.py
@@ -4,15 +4,15 @@ import contextlib
import functools
import os
import sys
-from typing import Generator
-from typing import Sequence
+from collections.abc import Generator
+from collections.abc import Sequence
import pre_commit.constants as C
+from pre_commit import lang_base
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
@@ -59,14 +59,14 @@ 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)
+def in_env(prefix: Prefix, version: str) -> Generator[None]:
+ envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir)):
yield
-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
@@ -93,16 +93,16 @@ def install_environment(
# install as if we installed from git
local_install_cmd = (
- 'npm', 'install', '--dev', '--prod',
+ 'npm', 'install', '--include=dev', '--include=prod',
'--ignore-prepublish', '--no-progress', '--no-save',
)
- 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 2530c0ee..a07d442a 100644
--- a/pre_commit/languages/perl.py
+++ b/pre_commit/languages/perl.py
@@ -3,19 +3,19 @@ from __future__ import annotations
import contextlib
import os
import shlex
-from typing import Generator
-from typing import Sequence
+from collections.abc import Generator
+from collections.abc import Sequence
+from pre_commit import lang_base
from pre_commit.envcontext import envcontext
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:
@@ -33,8 +33,8 @@ 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)
+def in_env(prefix: Prefix, version: str) -> Generator[None]:
+ envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir)):
yield
@@ -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 f0eb9a95..72a9345f 100644
--- a/pre_commit/languages/pygrep.py
+++ b/pre_commit/languages/pygrep.py
@@ -3,20 +3,20 @@ from __future__ import annotations
import argparse
import re
import sys
+from collections.abc import Sequence
+from re import Pattern
from typing import NamedTuple
-from typing import Pattern
-from typing import Sequence
+from pre_commit import lang_base
from pre_commit import output
-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 c373646b..88ececce 100644
--- a/pre_commit/languages/python.py
+++ b/pre_commit/languages/python.py
@@ -4,15 +4,15 @@ import contextlib
import functools
import os
import sys
-from typing import Generator
-from typing import Sequence
+from collections.abc import Generator
+from collections.abc import Sequence
import pre_commit.constants as C
+from pre_commit import lang_base
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,10 +21,10 @@ 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)
+@functools.cache
def _version_info(exe: str) -> str:
prog = 'import sys;print(".".join(str(p) for p in sys.version_info))'
try:
@@ -48,7 +48,7 @@ def _read_pyvenv_cfg(filename: str) -> dict[str, str]:
def bin_dir(venv: str) -> str:
"""On windows there's a different directory for the virtualenv"""
- bin_part = 'Scripts' if os.name == 'nt' else 'bin'
+ bin_part = 'Scripts' if sys.platform == 'win32' else 'bin'
return os.path.join(venv, bin_part)
@@ -65,7 +65,7 @@ def _find_by_py_launcher(
version: str,
) -> str | None: # pragma: no cover (windows only)
if version.startswith('python'):
- num = version[len('python'):]
+ num = version.removeprefix('python')
cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)')
env = dict(os.environ, PYTHONIOENCODING='UTF-8')
try:
@@ -75,6 +75,13 @@ def _find_by_py_launcher(
return None
+def _impl_exe_name() -> str:
+ if sys.implementation.name == 'cpython': # pragma: cpython cover
+ return 'python'
+ else: # pragma: cpython no cover
+ return sys.implementation.name # pypy mostly
+
+
def _find_by_sys_executable() -> str | None:
def _norm(path: str) -> str | None:
_, exe = os.path.split(path.lower())
@@ -100,18 +107,25 @@ def _find_by_sys_executable() -> str | None:
@functools.lru_cache(maxsize=1)
def get_default_version() -> str: # pragma: no cover (platform dependent)
- # First attempt from `sys.executable` (or the realpath)
- exe = _find_by_sys_executable()
- if exe:
- return exe
+ v_major = f'{sys.version_info[0]}'
+ v_minor = f'{sys.version_info[0]}.{sys.version_info[1]}'
- # Next try the `pythonX.X` executable
- exe = f'python{sys.version_info[0]}.{sys.version_info[1]}'
- if find_executable(exe):
- return exe
+ # attempt the likely implementation exe
+ for potential in (v_minor, v_major):
+ exe = f'{_impl_exe_name()}{potential}'
+ if find_executable(exe):
+ return exe
- if _find_by_py_launcher(exe):
- return exe
+ # next try `sys.executable` (or the realpath)
+ maybe_exe = _find_by_sys_executable()
+ if maybe_exe:
+ return maybe_exe
+
+ # maybe on windows we can find it via py launcher?
+ if sys.platform == 'win32': # pragma: win32 cover
+ exe = f'python{v_minor}'
+ if _find_by_py_launcher(exe):
+ return exe
# We tried!
return C.DEFAULT
@@ -124,7 +138,7 @@ def _sys_executable_matches(version: str) -> bool:
return False
try:
- info = tuple(int(p) for p in version[len('python'):].split('.'))
+ info = tuple(int(p) for p in version.removeprefix('python').split('.'))
except ValueError:
return False
@@ -137,7 +151,7 @@ def norm_version(version: str) -> str | None:
elif _sys_executable_matches(version): # virtualenv defaults to our exe
return None
- if os.name == 'nt': # pragma: no cover (windows)
+ if sys.platform == 'win32': # pragma: no cover (windows)
version_exec = _find_by_py_launcher(version)
if version_exec:
return version_exec
@@ -152,14 +166,14 @@ 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)
+def in_env(prefix: Prefix, version: str) -> Generator[None]:
+ envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir)):
yield
-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 +216,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 +225,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 e2383658..f70d2fdc 100644
--- a/pre_commit/languages/r.py
+++ b/pre_commit/languages/r.py
@@ -4,21 +4,120 @@ import contextlib
import os
import shlex
import shutil
-from typing import Generator
-from typing import Sequence
+import tempfile
+import textwrap
+from collections.abc import Generator
+from collections.abc import Sequence
+from pre_commit import lang_base
from pre_commit.envcontext import envcontext
from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import UNSET
-from pre_commit.languages import helpers
from pre_commit.prefix import Prefix
-from pre_commit.util import cmd_output_b
+from pre_commit.util import cmd_output
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
+
+_RENV_ACTIVATED_OPTS = (
+ '--no-save', '--no-restore', '--no-site-file', '--no-environ',
+)
+
+
+def _execute_r(
+ code: str, *,
+ prefix: Prefix, version: str, args: Sequence[str] = (), cwd: str,
+ cli_opts: Sequence[str],
+) -> str:
+ with in_env(prefix, version), _r_code_in_tempfile(code) as f:
+ _, out, _ = cmd_output(
+ _rscript_exec(), *cli_opts, f, *args, cwd=cwd,
+ )
+ return out.rstrip('\n')
+
+
+def _execute_r_in_renv(
+ code: str, *,
+ prefix: Prefix, version: str, args: Sequence[str] = (), cwd: str,
+) -> str:
+ return _execute_r(
+ code=code, prefix=prefix, version=version, args=args, cwd=cwd,
+ cli_opts=_RENV_ACTIVATED_OPTS,
+ )
+
+
+def _execute_vanilla_r(
+ code: str, *,
+ prefix: Prefix, version: str, args: Sequence[str] = (), cwd: str,
+) -> str:
+ return _execute_r(
+ code=code, prefix=prefix, version=version, args=args, cwd=cwd,
+ cli_opts=('--vanilla',),
+ )
+
+
+def _read_installed_version(envdir: str, prefix: Prefix, version: str) -> str:
+ return _execute_r_in_renv(
+ 'cat(renv::settings$r.version())',
+ prefix=prefix, version=version,
+ cwd=envdir,
+ )
+
+
+def _read_executable_version(envdir: str, prefix: Prefix, version: str) -> str:
+ return _execute_r_in_renv(
+ 'cat(as.character(getRversion()))',
+ prefix=prefix, version=version,
+ cwd=envdir,
+ )
+
+
+def _write_current_r_version(
+ envdir: str, prefix: Prefix, version: str,
+) -> None:
+ _execute_r_in_renv(
+ 'renv::settings$r.version(as.character(getRversion()))',
+ prefix=prefix, version=version,
+ cwd=envdir,
+ )
+
+
+def health_check(prefix: Prefix, version: str) -> str | None:
+ envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
+
+ r_version_installation = _read_installed_version(
+ envdir=envdir, prefix=prefix, version=version,
+ )
+ r_version_current_executable = _read_executable_version(
+ envdir=envdir, prefix=prefix, version=version,
+ )
+ if r_version_installation in {'NULL', ''}:
+ return (
+ f'Hooks were installed with an unknown R version. R version for '
+ f'hook repo now set to {r_version_current_executable}'
+ )
+ elif r_version_installation != r_version_current_executable:
+ return (
+ f'Hooks were installed for R version {r_version_installation}, '
+ f'but current R executable has version '
+ f'{r_version_current_executable}'
+ )
+
+ return None
+
+
+@contextlib.contextmanager
+def _r_code_in_tempfile(code: str) -> Generator[str]:
+ """
+ To avoid quoting and escaping issues, avoid `Rscript [options] -e {expr}`
+ but use `Rscript [options] path/to/file_with_expr.R`
+ """
+ with tempfile.TemporaryDirectory() as tmpdir:
+ fname = os.path.join(tmpdir, 'script.R')
+ with open(fname, 'w') as f:
+ f.write(_inline_r_setup(textwrap.dedent(code)))
+ yield fname
def get_env_patch(venv: str) -> PatchesT:
@@ -29,8 +128,8 @@ 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)
+def in_env(prefix: Prefix, version: str) -> Generator[None]:
+ envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir)):
yield
@@ -85,7 +184,7 @@ def _cmd_from_hook(
_entry_validate(cmd)
cmd_part = _prefix_if_file_entry(cmd, prefix, is_local=is_local)
- return (cmd[0], *RSCRIPT_OPTS, *cmd_part, *args)
+ return (cmd[0], *_RENV_ACTIVATED_OPTS, *cmd_part, *args)
def install_environment(
@@ -93,7 +192,9 @@ def install_environment(
version: str,
additional_dependencies: Sequence[str],
) -> None:
- env_dir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version)
+ lang_base.assert_version_default('r', version)
+
+ env_dir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
os.makedirs(env_dir, exist_ok=True)
shutil.copy(prefix.path('renv.lock'), env_dir)
shutil.copytree(prefix.path('renv'), os.path.join(env_dir, 'renv'))
@@ -126,21 +227,19 @@ def install_environment(
renv::install(prefix_dir)
}}
"""
-
- cmd_output_b(
- _rscript_exec(), '--vanilla', '-e',
- _inline_r_setup(r_code_inst_environment),
- cwd=env_dir,
+ _execute_vanilla_r(
+ r_code_inst_environment,
+ prefix=prefix, version=version, cwd=env_dir,
)
+
+ _write_current_r_version(envdir=env_dir, prefix=prefix, version=version)
if additional_dependencies:
r_code_inst_add = 'renv::install(commandArgs(trailingOnly = TRUE))'
- with in_env(prefix, version):
- cmd_output_b(
- _rscript_exec(), *RSCRIPT_OPTS, '-e',
- _inline_r_setup(r_code_inst_add),
- *additional_dependencies,
- cwd=env_dir,
- )
+ _execute_r_in_renv(
+ code=r_code_inst_add, prefix=prefix, version=version,
+ args=additional_dependencies,
+ cwd=env_dir,
+ )
def _inline_r_setup(code: str) -> str:
@@ -148,11 +247,16 @@ def _inline_r_setup(code: str) -> str:
Some behaviour of R cannot be configured via env variables, but can
only be configured via R options once R has started. These are set here.
"""
- with_option = f"""\
- options(install.packages.compile.from.source = "never", pkgType = "binary")
- {code}
- """
- return with_option
+ with_option = [
+ textwrap.dedent("""\
+ options(
+ install.packages.compile.from.source = "never",
+ pkgType = "binary"
+ )
+ """),
+ code,
+ ]
+ return '\n'.join(with_option)
def run_hook(
@@ -166,7 +270,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 b4d4b45a..f32fea3f 100644
--- a/pre_commit/languages/ruby.py
+++ b/pre_commit/languages/ruby.py
@@ -2,30 +2,36 @@ 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 Sequence
+from collections.abc import Generator
+from collections.abc import Sequence
+from typing import IO
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]:
+ files = importlib.resources.files('pre_commit.resources')
+ return files.joinpath(filename).open('rb')
@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
@@ -39,7 +45,6 @@ def get_env_patch(
('GEM_HOME', os.path.join(venv, 'gems')),
('GEM_PATH', UNSET),
('BUNDLE_IGNORE_CONFIG', '1'),
- ('BUNDLE_GEMFILE', os.devnull),
)
if language_version == 'system':
patches += (
@@ -68,14 +73,14 @@ 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)
+def in_env(prefix: Prefix, version: str) -> Generator[None]:
+ envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir, version)):
yield
def _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)
@@ -84,7 +89,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)
@@ -101,36 +106,40 @@ 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(
prefix: Prefix, version: str, additional_dependencies: Sequence[str],
) -> None:
+ envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
+
if version != 'system': # pragma: win32 no cover
_install_rbenv(prefix, version)
with in_env(prefix, version):
# 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',
'--no-document', '--no-format-executable',
'--no-user-install',
+ '--install-dir', os.path.join(envdir, 'gems'),
+ '--bindir', os.path.join(envdir, 'gems', 'bin'),
*prefix.star('.gem'), *additional_dependencies,
),
)
diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py
index 391fd865..fd77a9d2 100644
--- a/pre_commit/languages/rust.py
+++ b/pre_commit/languages/rust.py
@@ -7,23 +7,23 @@ import shutil
import sys
import tempfile
import urllib.request
-from typing import Generator
-from typing import Sequence
+from collections.abc import Generator
+from collections.abc import Sequence
import pre_commit.constants as C
+from pre_commit import lang_base
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)
@@ -34,7 +34,7 @@ def get_default_version() -> str:
# Just detecting the executable does not suffice, because if rustup is
# installed but no toolchain is available, then `cargo` exists but
# cannot be used without installing a toolchain first.
- if cmd_output_b('cargo', '--version', check=False)[0] == 0:
+ if cmd_output_b('cargo', '--version', check=False, cwd='/')[0] == 0:
return 'system'
else:
return C.DEFAULT
@@ -50,7 +50,6 @@ def _rust_toolchain(language_version: str) -> str:
def get_env_patch(target_dir: str, version: str) -> PatchesT:
return (
- ('CARGO_HOME', target_dir),
('PATH', (os.path.join(target_dir, 'bin'), os.pathsep, Var('PATH'))),
# Only set RUSTUP_TOOLCHAIN if we don't want use the system's default
# toolchain
@@ -62,8 +61,8 @@ 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)
+def in_env(prefix: Prefix, version: str) -> Generator[None]:
+ envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir, version)):
yield
@@ -78,12 +77,12 @@ 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:
+def install_rust_with_toolchain(toolchain: str, envdir: str) -> None:
with tempfile.TemporaryDirectory() as rustup_dir:
- with envcontext((('RUSTUP_HOME', rustup_dir),)):
+ with envcontext((('CARGO_HOME', envdir), ('RUSTUP_HOME', rustup_dir))):
# acquire `rustup` if not present
if parse_shebang.find_executable('rustup') is None:
# We did not detect rustup and need to download it first.
@@ -116,7 +115,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
@@ -135,16 +134,21 @@ def install_environment(
packages_to_install: set[tuple[str, ...]] = {('--path', '.')}
for cli_dep in cli_deps:
- cli_dep = cli_dep[len('cli:'):]
+ cli_dep = cli_dep.removeprefix('cli:')
package, _, crate_version = cli_dep.partition(':')
if crate_version != '':
packages_to_install.add((package, '--version', crate_version))
else:
packages_to_install.add((package,))
- with in_env(prefix, version):
+ with contextlib.ExitStack() as ctx:
+ ctx.enter_context(in_env(prefix, version))
+
if version != 'system':
- install_rust_with_toolchain(_rust_toolchain(version))
+ install_rust_with_toolchain(_rust_toolchain(version), envdir)
+
+ tmpdir = ctx.enter_context(tempfile.TemporaryDirectory())
+ ctx.enter_context(envcontext((('RUSTUP_HOME', tmpdir),)))
if len(lib_deps) > 0:
_add_dependencies(prefix, lib_deps)
diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py
index c66ad5fb..08a9c39a 100644
--- a/pre_commit/languages/swift.py
+++ b/pre_commit/languages/swift.py
@@ -2,13 +2,13 @@ from __future__ import annotations
import contextlib
import os
-from typing import Generator
-from typing import Sequence
+from collections.abc import Generator
+from collections.abc import Sequence
+from pre_commit import lang_base
from pre_commit.envcontext import envcontext
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
@@ -27,8 +27,8 @@ 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)
+def in_env(prefix: Prefix, version: str) -> Generator[None]:
+ envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir)):
yield
@@ -36,15 +36,15 @@ 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)
cmd_output_b(
'swift', 'build',
- '-C', prefix.prefix_dir,
+ '--package-path', prefix.prefix_dir,
'-c', BUILD_CONFIG,
'--build-path', os.path.join(envdir, BUILD_DIR),
)
diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py
deleted file mode 100644
index 204cad72..00000000
--- a/pre_commit/languages/system.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from __future__ import annotations
-
-from pre_commit.languages import helpers
-
-ENVIRONMENT_DIR = None
-get_default_version = helpers.basic_get_default_version
-health_check = helpers.basic_health_check
-install_environment = helpers.no_install
-in_env = helpers.no_env
-run_hook = helpers.basic_run_hook
diff --git a/pre_commit/languages/unsupported.py b/pre_commit/languages/unsupported.py
new file mode 100644
index 00000000..f6ad688f
--- /dev/null
+++ b/pre_commit/languages/unsupported.py
@@ -0,0 +1,10 @@
+from __future__ import annotations
+
+from pre_commit import lang_base
+
+ENVIRONMENT_DIR = None
+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/languages/script.py b/pre_commit/languages/unsupported_script.py
similarity index 59%
rename from pre_commit/languages/script.py
rename to pre_commit/languages/unsupported_script.py
index 08325f46..1eaa1e27 100644
--- a/pre_commit/languages/script.py
+++ b/pre_commit/languages/unsupported_script.py
@@ -1,15 +1,15 @@
from __future__ import annotations
-from typing import Sequence
+from collections.abc 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/logging_handler.py b/pre_commit/logging_handler.py
index 1b68fc7d..74772bee 100644
--- a/pre_commit/logging_handler.py
+++ b/pre_commit/logging_handler.py
@@ -2,7 +2,7 @@ from __future__ import annotations
import contextlib
import logging
-from typing import Generator
+from collections.abc import Generator
from pre_commit import color
from pre_commit import output
@@ -32,7 +32,7 @@ class LoggingHandler(logging.Handler):
@contextlib.contextmanager
-def logging_handler(use_color: bool) -> Generator[None, None, None]:
+def logging_handler(use_color: bool) -> Generator[None]:
handler = LoggingHandler(use_color)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
diff --git a/pre_commit/main.py b/pre_commit/main.py
index 3915993f..0c3eefda 100644
--- a/pre_commit/main.py
+++ b/pre_commit/main.py
@@ -4,11 +4,13 @@ import argparse
import logging
import os
import sys
-from typing import Sequence
+from collections.abc import Sequence
import pre_commit.constants as C
+from pre_commit import clientlib
from pre_commit import git
from pre_commit.color import add_color_option
+from pre_commit.commands import hazmat
from pre_commit.commands.autoupdate import autoupdate
from pre_commit.commands.clean import clean
from pre_commit.commands.gc import gc
@@ -36,8 +38,11 @@ logger = logging.getLogger('pre_commit')
# pyvenv
os.environ.pop('__PYVENV_LAUNCHER__', None)
+# https://github.com/getsentry/snuba/pull/5388
+os.environ.pop('PYTHONEXECUTABLE', None)
+
COMMANDS_NO_GIT = {
- 'clean', 'gc', 'init-templatedir', 'sample-config',
+ 'clean', 'gc', 'hazmat', 'init-templatedir', 'sample-config',
'validate-config', 'validate-manifest',
}
@@ -52,16 +57,16 @@ def _add_config_option(parser: argparse.ArgumentParser) -> None:
def _add_hook_type_option(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
'-t', '--hook-type',
- choices=C.HOOK_TYPES, action='append', dest='hook_types',
+ choices=clientlib.HOOK_TYPES, action='append', dest='hook_types',
)
def _add_run_options(parser: argparse.ArgumentParser) -> None:
parser.add_argument('hook', nargs='?', help='A single hook-id to run')
- parser.add_argument('--verbose', '-v', action='store_true', default=False)
+ parser.add_argument('--verbose', '-v', action='store_true')
mutex_group = parser.add_mutually_exclusive_group(required=False)
mutex_group.add_argument(
- '--all-files', '-a', action='store_true', default=False,
+ '--all-files', '-a', action='store_true',
help='Run on all the files in the repo.',
)
mutex_group.add_argument(
@@ -73,7 +78,14 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None:
help='When hooks fail, run `git diff` directly afterward.',
)
parser.add_argument(
- '--hook-stage', choices=C.STAGES, default='commit',
+ '--fail-fast', action='store_true',
+ help='Stop after the first failing hook.',
+ )
+ parser.add_argument(
+ '--hook-stage',
+ choices=clientlib.STAGES,
+ type=clientlib.transform_stage,
+ default='pre-commit',
help='The stage during which the hook is fired. One of %(choices)s',
)
parser.add_argument(
@@ -103,6 +115,17 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None:
'now checked out.'
),
)
+ parser.add_argument(
+ '--pre-rebase-upstream', help=(
+ 'The upstream from which the series was forked.'
+ ),
+ )
+ parser.add_argument(
+ '--pre-rebase-branch', help=(
+ 'The branch being rebased, and is not set when '
+ 'rebasing the current branch.'
+ ),
+ )
parser.add_argument(
'--commit-msg-filename',
help='Filename to check when running during `commit-msg`',
@@ -211,14 +234,23 @@ def main(argv: Sequence[str] | None = None) -> int:
help='Store "frozen" hashes in `rev` instead of tag names',
)
autoupdate_parser.add_argument(
- '--repo', dest='repos', action='append', metavar='REPO',
+ '--repo', dest='repos', action='append', metavar='REPO', default=[],
help='Only update this repository -- may be specified multiple times.',
)
+ autoupdate_parser.add_argument(
+ '-j', '--jobs', type=int, default=1,
+ help='Number of threads to use. (default %(default)s).',
+ )
_add_cmd('clean', help='Clean out pre-commit files.')
_add_cmd('gc', help='Clean unused cached repos.')
+ hazmat_parser = _add_cmd(
+ 'hazmat', help='Composable tools for rare use in hook `entry`.',
+ )
+ hazmat.add_parsers(hazmat_parser)
+
init_templatedir_parser = _add_cmd(
'init-templatedir',
help=(
@@ -253,7 +285,7 @@ def main(argv: Sequence[str] | None = None) -> int:
)
_add_hook_type_option(install_parser)
install_parser.add_argument(
- '--allow-missing-config', action='store_true', default=False,
+ '--allow-missing-config', action='store_true',
help=(
'Whether to allow a missing `pre-commit` configuration file '
'or exit with a failure code.'
@@ -353,15 +385,18 @@ def main(argv: Sequence[str] | None = None) -> int:
if args.command == 'autoupdate':
return autoupdate(
- args.config, store,
+ args.config,
tags_only=not args.bleeding_edge,
freeze=args.freeze,
repos=args.repos,
+ jobs=args.jobs,
)
elif args.command == 'clean':
return clean(store)
elif args.command == 'gc':
return gc(store)
+ elif args.command == 'hazmat':
+ return hazmat.impl(args)
elif args.command == 'hook-impl':
return hook_impl(
store,
diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py
index b05a7050..84c142b4 100644
--- a/pre_commit/meta_hooks/check_hooks_apply.py
+++ b/pre_commit/meta_hooks/check_hooks_apply.py
@@ -1,7 +1,7 @@
from __future__ import annotations
import argparse
-from typing import Sequence
+from collections.abc import Sequence
import pre_commit.constants as C
from pre_commit import git
@@ -21,7 +21,7 @@ def check_all_hooks_match_files(config_file: str) -> int:
for hook in all_hooks(config, Store()):
if hook.always_run or hook.language == 'fail':
continue
- elif not classifier.filenames_for_hook(hook):
+ elif not any(classifier.filenames_for_hook(hook)):
print(f'{hook.id} does not apply to this repository')
retv = 1
diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py
index 0a8249b8..664251a4 100644
--- a/pre_commit/meta_hooks/check_useless_excludes.py
+++ b/pre_commit/meta_hooks/check_useless_excludes.py
@@ -2,7 +2,8 @@ from __future__ import annotations
import argparse
import re
-from typing import Sequence
+from collections.abc import Iterable
+from collections.abc import Sequence
from cfgv import apply_defaults
@@ -14,7 +15,7 @@ from pre_commit.commands.run import Classifier
def exclude_matches_any(
- filenames: Sequence[str],
+ filenames: Iterable[str],
include: str,
exclude: str,
) -> bool:
@@ -50,11 +51,12 @@ def check_useless_excludes(config_file: str) -> int:
# Not actually a manifest dict, but this more accurately reflects
# the defaults applied during runtime
hook = apply_defaults(hook, MANIFEST_HOOK_DICT)
- names = classifier.filenames
- types = hook['types']
- types_or = hook['types_or']
- exclude_types = hook['exclude_types']
- names = classifier.by_types(names, types, types_or, exclude_types)
+ names = classifier.by_types(
+ classifier.filenames,
+ hook['types'],
+ hook['types_or'],
+ hook['exclude_types'],
+ )
include, exclude = hook['files'], hook['exclude']
if not exclude_matches_any(names, include, exclude):
print(
diff --git a/pre_commit/meta_hooks/identity.py b/pre_commit/meta_hooks/identity.py
index 72ee440b..3e20bbc6 100644
--- a/pre_commit/meta_hooks/identity.py
+++ b/pre_commit/meta_hooks/identity.py
@@ -1,7 +1,7 @@
from __future__ import annotations
import sys
-from typing import Sequence
+from collections.abc import Sequence
from pre_commit import output
diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py
index 3ee04e8d..043a9b5d 100644
--- a/pre_commit/parse_shebang.py
+++ b/pre_commit/parse_shebang.py
@@ -1,7 +1,7 @@
from __future__ import annotations
import os.path
-from typing import Mapping
+from collections.abc import Mapping
from typing import NoReturn
from identify.identify import parse_shebang_from_file
diff --git a/pre_commit/repository.py b/pre_commit/repository.py
index 616faf54..a9461ab6 100644
--- a/pre_commit/repository.py
+++ b/pre_commit/repository.py
@@ -3,17 +3,16 @@ from __future__ import annotations
import json
import logging
import os
+from collections.abc import Sequence
from typing import Any
-from typing import Sequence
import pre_commit.constants as C
+from pre_commit.all_languages import languages
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 +31,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:
@@ -115,15 +114,6 @@ def _hook(
for dct in rest:
ret.update(dct)
- version = ret['minimum_pre_commit_version']
- if parse_version(version) > parse_version(C.VERSION):
- logger.error(
- f'The hook `{ret["id"]}` requires pre-commit version {version} '
- f'but version {C.VERSION} is installed. '
- f'Perhaps run `pip install --upgrade pre-commit`.',
- )
- exit(1)
-
lang = ret['language']
if ret['language_version'] == C.DEFAULT:
ret['language_version'] = root_config['default_language_version'][lang]
diff --git a/pre_commit/resources/empty_template_pubspec.yaml b/pre_commit/resources/empty_template_pubspec.yaml
index 3be6ffe3..8306aeb6 100644
--- a/pre_commit/resources/empty_template_pubspec.yaml
+++ b/pre_commit/resources/empty_template_pubspec.yaml
@@ -1,4 +1,4 @@
name: pre_commit_empty_pubspec
environment:
- sdk: '>=2.10.0'
+ sdk: '>=2.12.0'
executables: {}
diff --git a/pre_commit/resources/empty_template_setup.py b/pre_commit/resources/empty_template_setup.py
index ef05eef8..e8b1ff02 100644
--- a/pre_commit/resources/empty_template_setup.py
+++ b/pre_commit/resources/empty_template_setup.py
@@ -1,4 +1,4 @@
from setuptools import setup
-setup(name='pre-commit-placeholder-package', version='0.0.0')
+setup(name='pre-commit-placeholder-package', version='0.0.0', py_modules=[])
diff --git a/pre_commit/resources/rbenv.tar.gz b/pre_commit/resources/rbenv.tar.gz
index da2514e7..b5df0874 100644
Binary files a/pre_commit/resources/rbenv.tar.gz and b/pre_commit/resources/rbenv.tar.gz differ
diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz
index b6eacf59..5c82c906 100644
Binary files a/pre_commit/resources/ruby-build.tar.gz and b/pre_commit/resources/ruby-build.tar.gz differ
diff --git a/pre_commit/resources/ruby-download.tar.gz b/pre_commit/resources/ruby-download.tar.gz
index 92502a77..f7cb0b42 100644
Binary files a/pre_commit/resources/ruby-download.tar.gz and b/pre_commit/resources/ruby-download.tar.gz differ
diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py
index 172fb20b..99ea0979 100644
--- a/pre_commit/staged_files_only.py
+++ b/pre_commit/staged_files_only.py
@@ -4,9 +4,10 @@ import contextlib
import logging
import os.path
import time
-from typing import Generator
+from collections.abc import Generator
from pre_commit import git
+from pre_commit.errors import FatalError
from pre_commit.util import CalledProcessError
from pre_commit.util import cmd_output
from pre_commit.util import cmd_output_b
@@ -32,7 +33,7 @@ def _git_apply(patch: str) -> None:
@contextlib.contextmanager
-def _intent_to_add_cleared() -> Generator[None, None, None]:
+def _intent_to_add_cleared() -> Generator[None]:
intent_to_add = git.intent_to_add_files()
if intent_to_add:
logger.warning('Unstaged intent-to-add files detected.')
@@ -47,14 +48,23 @@ def _intent_to_add_cleared() -> Generator[None, None, None]:
@contextlib.contextmanager
-def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]:
+def _unstaged_changes_cleared(patch_dir: str) -> Generator[None]:
tree = cmd_output('git', 'write-tree')[1].strip()
- 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 not diff_stdout.strip():
+ # due to behaviour (probably a bug?) in git with crlf endings and
+ # autocrlf set to either `true` or `input` sometimes git will refuse
+ # to show a crlf-only diff to us :(
+ yield
+ elif retcode == 1 and diff_stdout.strip():
patch_filename = f'patch{int(time.time())}-{os.getpid()}'
patch_filename = os.path.join(patch_dir, patch_filename)
logger.warning('Unstaged files detected.')
@@ -62,7 +72,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,14 +96,16 @@ 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
-def staged_files_only(patch_dir: str) -> Generator[None, None, None]:
+def staged_files_only(patch_dir: str) -> Generator[None]:
"""Clear any unstaged changes from the git working directory inside this
context.
"""
diff --git a/pre_commit/store.py b/pre_commit/store.py
index 6ddc7c48..dc90c051 100644
--- a/pre_commit/store.py
+++ b/pre_commit/store.py
@@ -5,18 +5,18 @@ import logging
import os.path
import sqlite3
import tempfile
-from typing import Callable
-from typing import Generator
-from typing import Sequence
+from collections.abc import Callable
+from collections.abc import Generator
+from collections.abc import Sequence
import pre_commit.constants as C
+from pre_commit import clientlib
from pre_commit import file_lock
from pre_commit import git
from pre_commit.util import CalledProcessError
from pre_commit.util import clean_path_on_failure
from pre_commit.util import cmd_output_b
from pre_commit.util import resource_text
-from pre_commit.util import rmtree
logger = logging.getLogger('pre_commit')
@@ -95,13 +95,13 @@ class Store:
' PRIMARY KEY (repo, ref)'
');',
)
- self._create_config_table(db)
+ self._create_configs_table(db)
# Atomic file move
os.replace(tmpfile, self.db_path)
@contextlib.contextmanager
- def exclusive_lock(self) -> Generator[None, None, None]:
+ def exclusive_lock(self) -> Generator[None]:
def blocked_cb() -> None: # pragma: no cover (tests are in-process)
logger.info('Locking pre-commit directory')
@@ -112,7 +112,7 @@ class Store:
def connect(
self,
db_path: str | None = None,
- ) -> Generator[sqlite3.Connection, None, None]:
+ ) -> Generator[sqlite3.Connection]:
db_path = db_path or self.db_path
# sqlite doesn't close its fd with its contextmanager >.<
# contextlib.closing fixes this.
@@ -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
@@ -136,6 +136,7 @@ class Store:
deps: Sequence[str],
make_strategy: Callable[[str], None],
) -> str:
+ original_repo = repo
repo = self.db_repo_name(repo, deps)
def _get_result() -> str | None:
@@ -168,6 +169,9 @@ class Store:
'INSERT INTO repos (repo, ref, path) VALUES (?, ?, ?)',
[repo, ref, directory],
)
+
+ clientlib.warn_for_stages_on_repo_init(original_repo, directory)
+
return directory
def _complete_clone(self, ref: str, git_cmd: Callable[..., None]) -> None:
@@ -210,7 +214,7 @@ class Store:
'local', C.LOCAL_REPO_VERSION, deps, _make_local_repo,
)
- def _create_config_table(self, db: sqlite3.Connection) -> None:
+ def _create_configs_table(self, db: sqlite3.Connection) -> None:
db.executescript(
'CREATE TABLE IF NOT EXISTS configs ('
' path TEXT NOT NULL,'
@@ -227,28 +231,5 @@ class Store:
return
with self.connect() as db:
# TODO: eventually remove this and only create in _create
- self._create_config_table(db)
+ self._create_configs_table(db)
db.execute('INSERT OR IGNORE INTO configs VALUES (?)', (path,))
-
- def select_all_configs(self) -> list[str]:
- with self.connect() as db:
- self._create_config_table(db)
- rows = db.execute('SELECT path FROM configs').fetchall()
- return [path for path, in rows]
-
- def delete_configs(self, configs: list[str]) -> None:
- with self.connect() as db:
- rows = [(path,) for path in configs]
- db.executemany('DELETE FROM configs WHERE path = ?', rows)
-
- def select_all_repos(self) -> list[tuple[str, str, str]]:
- with self.connect() as db:
- return db.execute('SELECT repo, ref, path from repos').fetchall()
-
- def delete_repo(self, db_repo_name: str, ref: str, path: str) -> None:
- with self.connect() as db:
- db.execute(
- 'DELETE FROM repos WHERE repo = ? and ref = ?',
- (db_repo_name, ref),
- )
- rmtree(path)
diff --git a/pre_commit/util.py b/pre_commit/util.py
index 8ea48446..19b1880b 100644
--- a/pre_commit/util.py
+++ b/pre_commit/util.py
@@ -8,11 +8,10 @@ import shutil
import stat
import subprocess
import sys
+from collections.abc import Callable
+from collections.abc import Generator
from types import TracebackType
from typing import Any
-from typing import Callable
-from typing import Generator
-from typing import IO
from pre_commit import parse_shebang
@@ -26,7 +25,7 @@ def force_bytes(exc: Any) -> bytes:
@contextlib.contextmanager
-def clean_path_on_failure(path: str) -> Generator[None, None, None]:
+def clean_path_on_failure(path: str) -> Generator[None]:
"""Cleans up the directory on an exceptional failure."""
try:
yield
@@ -36,12 +35,9 @@ 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)
+ files = importlib.resources.files('pre_commit.resources')
+ return files.joinpath(filename).read_text()
def make_executable(filename: str) -> None:
@@ -67,7 +63,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)'
@@ -124,7 +120,7 @@ def cmd_output(*cmd: str, **kwargs: Any) -> tuple[int, str, str | None]:
return returncode, stdout, stderr
-if os.name != 'nt': # pragma: win32 no cover
+if sys.platform != 'win32': # pragma: win32 no cover
from os import openpty
import termios
@@ -206,24 +202,37 @@ else: # pragma: no cover
cmd_output_p = cmd_output_b
-def rmtree(path: str) -> None:
- """On windows, rmtree fails for readonly dirs."""
- def handle_remove_readonly(
- func: Callable[..., Any],
- path: str,
- exc: tuple[type[OSError], OSError, TracebackType],
+def _handle_readonly(
+ func: Callable[[str], object],
+ path: str,
+ exc: BaseException,
+) -> None:
+ if (
+ func in (os.rmdir, os.remove, os.unlink) and
+ isinstance(exc, OSError) and
+ exc.errno in {errno.EACCES, errno.EPERM}
+ ):
+ for p in (path, os.path.dirname(path)):
+ os.chmod(p, os.stat(p).st_mode | stat.S_IWUSR)
+ func(path)
+ else:
+ raise
+
+
+if sys.version_info < (3, 12): # pragma: <3.12 cover
+ def _handle_readonly_old(
+ func: Callable[[str], object],
+ path: str,
+ excinfo: tuple[type[BaseException], BaseException, TracebackType],
) -> None:
- excvalue = exc[1]
- if (
- func in (os.rmdir, os.remove, os.unlink) and
- excvalue.errno in {errno.EACCES, errno.EPERM}
- ):
- for p in (path, os.path.dirname(path)):
- os.chmod(p, os.stat(p).st_mode | stat.S_IWUSR)
- func(path)
- else:
- raise
- shutil.rmtree(path, ignore_errors=False, onerror=handle_remove_readonly)
+ return _handle_readonly(func, path, excinfo[1])
+
+ def rmtree(path: str) -> None:
+ shutil.rmtree(path, ignore_errors=False, onerror=_handle_readonly_old)
+else: # pragma: >=3.12 cover
+ def rmtree(path: str) -> None:
+ """On windows, rmtree fails for readonly dirs."""
+ shutil.rmtree(path, ignore_errors=False, onexc=_handle_readonly)
def win_exe(s: str) -> str:
diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py
index e3af90ef..7c98d167 100644
--- a/pre_commit/xargs.py
+++ b/pre_commit/xargs.py
@@ -3,15 +3,16 @@ from __future__ import annotations
import concurrent.futures
import contextlib
import math
+import multiprocessing
import os
import subprocess
import sys
+from collections.abc import Callable
+from collections.abc import Generator
+from collections.abc import Iterable
+from collections.abc import MutableMapping
+from collections.abc import Sequence
from typing import Any
-from typing import Callable
-from typing import Generator
-from typing import Iterable
-from typing import MutableMapping
-from typing import Sequence
from typing import TypeVar
from pre_commit import parse_shebang
@@ -22,6 +23,21 @@ TArg = TypeVar('TArg')
TRet = TypeVar('TRet')
+def cpu_count() -> int:
+ try:
+ # On systems that support it, this will return a more accurate count of
+ # usable CPUs for the current process, which will take into account
+ # cgroup limits
+ return len(os.sched_getaffinity(0))
+ except AttributeError:
+ pass
+
+ try:
+ return multiprocessing.cpu_count()
+ except NotImplementedError:
+ return 1
+
+
def _environ_size(_env: MutableMapping[str, str] | None = None) -> int:
environ = _env if _env is not None else getattr(os, 'environb', os.environ)
size = 8 * len(environ) # number of pointers in `envp`
@@ -104,7 +120,6 @@ def partition(
@contextlib.contextmanager
def _thread_mapper(maxsize: int) -> Generator[
Callable[[Callable[[TArg], TRet], Iterable[TArg]], Iterable[TRet]],
- None, None,
]:
if maxsize == 1:
yield map
@@ -162,7 +177,8 @@ def xargs(
results = thread_map(run_cmd_partition, partitions)
for proc_retcode, proc_out, _ in results:
- retcode = max(retcode, proc_retcode)
+ if abs(proc_retcode) > abs(retcode):
+ retcode = proc_retcode
stdout += proc_out
return retcode, stdout
diff --git a/pre_commit/yaml.py b/pre_commit/yaml.py
index bdf4ec47..a5bbbc99 100644
--- a/pre_commit/yaml.py
+++ b/pre_commit/yaml.py
@@ -6,6 +6,7 @@ from typing import Any
import yaml
Loader = getattr(yaml, 'CSafeLoader', yaml.SafeLoader)
+yaml_compose = functools.partial(yaml.compose, Loader=Loader)
yaml_load = functools.partial(yaml.load, Loader=Loader)
Dumper = getattr(yaml, 'CSafeDumper', yaml.SafeDumper)
diff --git a/pre_commit/yaml_rewrite.py b/pre_commit/yaml_rewrite.py
new file mode 100644
index 00000000..8d0e8fdb
--- /dev/null
+++ b/pre_commit/yaml_rewrite.py
@@ -0,0 +1,52 @@
+from __future__ import annotations
+
+from collections.abc import Generator
+from collections.abc import Iterable
+from typing import NamedTuple
+from typing import Protocol
+
+from yaml.nodes import MappingNode
+from yaml.nodes import Node
+from yaml.nodes import ScalarNode
+from yaml.nodes import SequenceNode
+
+
+class _Matcher(Protocol):
+ def match(self, n: Node) -> Generator[Node]: ...
+
+
+class MappingKey(NamedTuple):
+ k: str
+
+ def match(self, n: Node) -> Generator[Node]:
+ if isinstance(n, MappingNode):
+ for k, _ in n.value:
+ if k.value == self.k:
+ yield k
+
+
+class MappingValue(NamedTuple):
+ k: str
+
+ def match(self, n: Node) -> Generator[Node]:
+ if isinstance(n, MappingNode):
+ for k, v in n.value:
+ if k.value == self.k:
+ yield v
+
+
+class SequenceItem(NamedTuple):
+ def match(self, n: Node) -> Generator[Node]:
+ if isinstance(n, SequenceNode):
+ yield from n.value
+
+
+def _match(gen: Iterable[Node], m: _Matcher) -> Iterable[Node]:
+ return (n for src in gen for n in m.match(src))
+
+
+def match(n: Node, matcher: tuple[_Matcher, ...]) -> Generator[ScalarNode]:
+ gen: Iterable[Node] = (n,)
+ for m in matcher:
+ gen = _match(gen, m)
+ return (n for n in gen if isinstance(n, ScalarNode))
diff --git a/setup.cfg b/setup.cfg
index 37511c09..a95ee447 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
[metadata]
name = pre_commit
-version = 3.0.2
+version = 4.5.1
description = A framework for managing and maintaining multi-language pre-commit hooks.
long_description = file: README.md
long_description_content_type = text/markdown
@@ -8,9 +8,8 @@ url = https://github.com/pre-commit/pre-commit
author = Anthony Sottile
author_email = asottile@umich.edu
license = MIT
-license_file = LICENSE
+license_files = LICENSE
classifiers =
- License :: OSI Approved :: MIT License
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: Implementation :: CPython
@@ -24,7 +23,7 @@ install_requires =
nodeenv>=0.11.1
pyyaml>=5.1
virtualenv>=20.10.0
-python_requires = >=3.8
+python_requires = >=3.10
[options.packages.find]
exclude =
@@ -53,6 +52,7 @@ check_untyped_defs = true
disallow_any_generics = true
disallow_incomplete_defs = true
disallow_untyped_defs = true
+enable_error_code = deprecated
warn_redundant_casts = true
warn_unused_ignores = true
diff --git a/testing/get-dart.sh b/testing/get-dart.sh
index 998b9d98..b4545e71 100755
--- a/testing/get-dart.sh
+++ b/testing/get-dart.sh
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
-VERSION=2.13.4
+VERSION=2.19.6
if [ "$OSTYPE" = msys ]; then
URL="https://storage.googleapis.com/dart-archive/channels/stable/release/${VERSION}/sdk/dartsdk-windows-x64-release.zip"
diff --git a/testing/get-swift.sh b/testing/get-swift.sh
deleted file mode 100755
index dfe09391..00000000
--- a/testing/get-swift.sh
+++ /dev/null
@@ -1,29 +0,0 @@
-#!/usr/bin/env bash
-# This is a script used in CI to install swift
-set -euo pipefail
-
-. /etc/lsb-release
-if [ "$DISTRIB_CODENAME" = "jammy" ]; then
- SWIFT_URL='https://download.swift.org/swift-5.7.1-release/ubuntu2204/swift-5.7.1-RELEASE/swift-5.7.1-RELEASE-ubuntu22.04.tar.gz'
- SWIFT_HASH='7f60291f5088d3e77b0c2364beaabd29616ee7b37260b7b06bdbeb891a7fe161'
-else
- echo "unknown dist: ${DISTRIB_CODENAME}" 1>&2
- exit 1
-fi
-
-check() {
- echo "$SWIFT_HASH $TGZ" | sha256sum --check
-}
-
-TGZ="$HOME/.swift/swift.tar.gz"
-mkdir -p "$(dirname "$TGZ")"
-if ! check >& /dev/null; then
- rm -f "$TGZ"
- curl --location --silent --output "$TGZ" "$SWIFT_URL"
- check
-fi
-
-mkdir -p /tmp/swift
-tar -xf "$TGZ" --strip 1 --directory /tmp/swift
-
-echo '/tmp/swift/usr/bin' >> "$GITHUB_PATH"
diff --git a/testing/language_helpers.py b/testing/language_helpers.py
index f9ae0b1d..05c94ebc 100644
--- a/testing/language_helpers.py
+++ b/testing/language_helpers.py
@@ -1,10 +1,9 @@
from __future__ import annotations
import os
-from typing import Sequence
+from collections.abc import Sequence
-import pre_commit.constants as C
-from pre_commit.languages.all import Language
+from pre_commit.lang_base import Language
from pre_commit.prefix import Prefix
@@ -14,13 +13,19 @@ def run_language(
exe: str,
args: Sequence[str] = (),
file_args: Sequence[str] = (),
- version: str = C.DEFAULT,
+ 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)
+ 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,
@@ -28,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 00000000..f4804c7e
--- /dev/null
+++ b/testing/languages
@@ -0,0 +1,92 @@
+#!/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 _always_run() -> frozenset[str]:
+ ret = ['.github/workflows/languages.yaml', 'testing/languages']
+ ret.extend(
+ os.path.join('pre_commit/resources', fname)
+ for fname in os.listdir('pre_commit/resources')
+ )
+ return frozenset(ret)
+
+
+def _lang_files(lang: str) -> frozenset[str]:
+ prog = f'''\
+import json
+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'
+ ]
+
+ triggers_all = _always_run()
+ for fname in triggers_all:
+ assert os.path.exists(fname), fname
+
+ if not args.all:
+ with concurrent.futures.ThreadPoolExecutor(os.cpu_count()) as exe:
+ by_lang = {
+ lang: files | triggers_all
+ 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/make-archives b/testing/make-archives
index cec9a9ff..10f40a3a 100755
--- a/testing/make-archives
+++ b/testing/make-archives
@@ -8,7 +8,7 @@ import shutil
import subprocess
import tarfile
import tempfile
-from typing import Sequence
+from collections.abc import Sequence
# This is a script for generating the tarred resources for git repo
@@ -16,8 +16,8 @@ from typing import Sequence
REPOS = (
- ('rbenv', 'https://github.com/rbenv/rbenv', '38e1fbb'),
- ('ruby-build', 'https://github.com/rbenv/ruby-build', '9d92a69'),
+ ('rbenv', 'https://github.com/rbenv/rbenv', '10e96bfc'),
+ ('ruby-build', 'https://github.com/rbenv/ruby-build', '447468b1'),
(
'ruby-download',
'https://github.com/garnieretienne/rvm-download',
@@ -57,8 +57,7 @@ def make_archive(name: str, repo: str, ref: str, destdir: str) -> str:
arcs.sort()
with gzip.GzipFile(output_path, 'wb', mtime=0) as gzipf:
- # https://github.com/python/typeshed/issues/5491
- with tarfile.open(fileobj=gzipf, mode='w') as tf: # type: ignore
+ with tarfile.open(fileobj=gzipf, mode='w') as tf:
for arcname, abspath in arcs:
tf.add(
abspath,
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 52957396..00000000
--- 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 0bd1de0c..00000000
--- 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 e9fb2456..00000000
--- 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 f221854a..00000000
--- 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 edb0fcbc..00000000
--- 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 03876f5c..00000000
--- 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 861ced6d..00000000
--- 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 47a99a35..00000000
--- 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 dfce2cad..00000000
--- 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 edcd28f4..00000000
--- 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 6626627d..00000000
--- 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 1456e8ef..00000000
--- 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 754b7600..00000000
--- 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 edcd28f4..00000000
--- 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 0f514c11..00000000
--- 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 1456e8ef..00000000
--- 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 fa9879b0..00000000
--- 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 edcd28f4..00000000
--- 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 0f514c11..00000000
--- 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 04ad4e0c..00000000
--- 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 a4e2d005..00000000
--- 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 87d2afba..00000000
--- 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 206733bb..00000000
--- 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 f37d4b67..00000000
--- 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 ec0c385a..00000000
--- 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 16857438..00000000
--- 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/node_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/node_hooks_repo/.pre-commit-hooks.yaml
deleted file mode 100644
index 257698a4..00000000
--- a/testing/resources/node_hooks_repo/.pre-commit-hooks.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-- id: foo
- name: Foo
- entry: foo
- language: node
- files: \.js$
diff --git a/testing/resources/node_hooks_repo/bin/main.js b/testing/resources/node_hooks_repo/bin/main.js
deleted file mode 100644
index 8e0f025a..00000000
--- a/testing/resources/node_hooks_repo/bin/main.js
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/usr/bin/env node
-
-console.log('Hello World');
diff --git a/testing/resources/node_hooks_repo/package.json b/testing/resources/node_hooks_repo/package.json
deleted file mode 100644
index 050b6300..00000000
--- a/testing/resources/node_hooks_repo/package.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name": "foo",
- "version": "0.0.1",
- "bin": {"foo": "./bin/main.js"}
-}
diff --git a/testing/resources/node_versioned_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/node_versioned_hooks_repo/.pre-commit-hooks.yaml
deleted file mode 100644
index e7ad5ea7..00000000
--- a/testing/resources/node_versioned_hooks_repo/.pre-commit-hooks.yaml
+++ /dev/null
@@ -1,6 +0,0 @@
-- id: versioned-node-hook
- name: Versioned node hook
- entry: versioned-node-hook
- language: node
- language_version: 9.3.0
- files: \.js$
diff --git a/testing/resources/node_versioned_hooks_repo/bin/main.js b/testing/resources/node_versioned_hooks_repo/bin/main.js
deleted file mode 100644
index df12cbeb..00000000
--- a/testing/resources/node_versioned_hooks_repo/bin/main.js
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/usr/bin/env node
-
-console.log(process.version);
-console.log('Hello World');
diff --git a/testing/resources/node_versioned_hooks_repo/package.json b/testing/resources/node_versioned_hooks_repo/package.json
deleted file mode 100644
index 18c7787c..00000000
--- a/testing/resources/node_versioned_hooks_repo/package.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name": "versioned-node-hook",
- "version": "0.0.1",
- "bin": {"versioned-node-hook": "./bin/main.js"}
-}
diff --git a/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml
deleted file mode 100644
index 2c237009..00000000
--- a/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml
+++ /dev/null
@@ -1,6 +0,0 @@
-- id: python3-hook
- name: Python 3 Hook
- entry: python3-hook
- language: python
- language_version: python3
- files: \.py$
diff --git a/testing/resources/python3_hooks_repo/py3_hook.py b/testing/resources/python3_hooks_repo/py3_hook.py
deleted file mode 100644
index 8c9cda4c..00000000
--- a/testing/resources/python3_hooks_repo/py3_hook.py
+++ /dev/null
@@ -1,8 +0,0 @@
-import sys
-
-
-def main():
- print(sys.version_info[0])
- print(repr(sys.argv[1:]))
- print('Hello World')
- return 0
diff --git a/testing/resources/python3_hooks_repo/setup.py b/testing/resources/python3_hooks_repo/setup.py
deleted file mode 100644
index 9125dc1d..00000000
--- a/testing/resources/python3_hooks_repo/setup.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from setuptools import setup
-
-setup(
- name='python3_hook',
- version='0.0.0',
- py_modules=['py3_hook'],
- entry_points={'console_scripts': ['python3-hook = py3_hook:main']},
-)
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 a666ed87..00000000
--- 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 40efde39..00000000
--- 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 cff6cadf..00000000
--- 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/resources/ruby_hooks_repo/.gitignore b/testing/resources/ruby_hooks_repo/.gitignore
deleted file mode 100644
index c111b331..00000000
--- a/testing/resources/ruby_hooks_repo/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-*.gem
diff --git a/testing/resources/ruby_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/ruby_hooks_repo/.pre-commit-hooks.yaml
deleted file mode 100644
index aa15872f..00000000
--- a/testing/resources/ruby_hooks_repo/.pre-commit-hooks.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-- id: ruby_hook
- name: Ruby Hook
- entry: ruby_hook
- language: ruby
- files: \.rb$
diff --git a/testing/resources/ruby_hooks_repo/bin/ruby_hook b/testing/resources/ruby_hooks_repo/bin/ruby_hook
deleted file mode 100755
index 5a7e5ed2..00000000
--- a/testing/resources/ruby_hooks_repo/bin/ruby_hook
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/usr/bin/env ruby
-
-puts 'Hello world from a ruby hook'
diff --git a/testing/resources/ruby_hooks_repo/lib/.gitignore b/testing/resources/ruby_hooks_repo/lib/.gitignore
deleted file mode 100644
index e69de29b..00000000
diff --git a/testing/resources/ruby_hooks_repo/ruby_hook.gemspec b/testing/resources/ruby_hooks_repo/ruby_hook.gemspec
deleted file mode 100644
index 75f4e8f7..00000000
--- a/testing/resources/ruby_hooks_repo/ruby_hook.gemspec
+++ /dev/null
@@ -1,9 +0,0 @@
-Gem::Specification.new do |s|
- s.name = 'ruby_hook'
- s.version = '0.1.0'
- s.authors = ['Anthony Sottile']
- s.summary = 'A ruby hook!'
- s.description = 'A ruby hook!'
- s.files = ['bin/ruby_hook']
- s.executables = ['ruby_hook']
-end
diff --git a/testing/resources/ruby_versioned_hooks_repo/.gitignore b/testing/resources/ruby_versioned_hooks_repo/.gitignore
deleted file mode 100644
index c111b331..00000000
--- a/testing/resources/ruby_versioned_hooks_repo/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-*.gem
diff --git a/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml
deleted file mode 100644
index c97939ad..00000000
--- a/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml
+++ /dev/null
@@ -1,6 +0,0 @@
-- id: ruby_hook
- name: Ruby Hook
- entry: ruby_hook
- language: ruby
- language_version: 3.2.0
- files: \.rb$
diff --git a/testing/resources/ruby_versioned_hooks_repo/bin/ruby_hook b/testing/resources/ruby_versioned_hooks_repo/bin/ruby_hook
deleted file mode 100755
index 2406f04c..00000000
--- a/testing/resources/ruby_versioned_hooks_repo/bin/ruby_hook
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/usr/bin/env ruby
-
-puts RUBY_VERSION
-puts 'Hello world from a ruby hook'
diff --git a/testing/resources/ruby_versioned_hooks_repo/lib/.gitignore b/testing/resources/ruby_versioned_hooks_repo/lib/.gitignore
deleted file mode 100644
index e69de29b..00000000
diff --git a/testing/resources/ruby_versioned_hooks_repo/ruby_hook.gemspec b/testing/resources/ruby_versioned_hooks_repo/ruby_hook.gemspec
deleted file mode 100644
index 75f4e8f7..00000000
--- a/testing/resources/ruby_versioned_hooks_repo/ruby_hook.gemspec
+++ /dev/null
@@ -1,9 +0,0 @@
-Gem::Specification.new do |s|
- s.name = 'ruby_hook'
- s.version = '0.1.0'
- s.authors = ['Anthony Sottile']
- s.summary = 'A ruby hook!'
- s.description = 'A ruby hook!'
- s.files = ['bin/ruby_hook']
- s.executables = ['ruby_hook']
-end
diff --git a/testing/resources/rust_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/rust_hooks_repo/.pre-commit-hooks.yaml
deleted file mode 100644
index df1269ff..00000000
--- a/testing/resources/rust_hooks_repo/.pre-commit-hooks.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-- id: rust-hook
- name: rust example hook
- entry: rust-hello-world
- language: rust
- files: ''
diff --git a/testing/resources/rust_hooks_repo/Cargo.lock b/testing/resources/rust_hooks_repo/Cargo.lock
deleted file mode 100644
index 36fbfda2..00000000
--- a/testing/resources/rust_hooks_repo/Cargo.lock
+++ /dev/null
@@ -1,3 +0,0 @@
-[[package]]
-name = "rust-hello-world"
-version = "0.1.0"
diff --git a/testing/resources/rust_hooks_repo/Cargo.toml b/testing/resources/rust_hooks_repo/Cargo.toml
deleted file mode 100644
index cd83b435..00000000
--- a/testing/resources/rust_hooks_repo/Cargo.toml
+++ /dev/null
@@ -1,3 +0,0 @@
-[package]
-name = "rust-hello-world"
-version = "0.1.0"
diff --git a/testing/resources/rust_hooks_repo/src/main.rs b/testing/resources/rust_hooks_repo/src/main.rs
deleted file mode 100644
index ad379d6e..00000000
--- a/testing/resources/rust_hooks_repo/src/main.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-fn main() {
- println!("hello world");
-}
diff --git a/testing/resources/system_hook_with_spaces_repo/.pre-commit-hooks.yaml b/testing/resources/system_hook_with_spaces_repo/.pre-commit-hooks.yaml
deleted file mode 100644
index b2c347c1..00000000
--- a/testing/resources/system_hook_with_spaces_repo/.pre-commit-hooks.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-- id: system-hook-with-spaces
- name: System hook with spaces
- entry: bash -c 'echo "Hello World"'
- language: system
- files: \.sh$
diff --git a/testing/util.py b/testing/util.py
index b6c3804e..1646ccd2 100644
--- a/testing/util.py
+++ b/testing/util.py
@@ -3,27 +3,17 @@ from __future__ import annotations
import contextlib
import os.path
import subprocess
+import sys
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,11 +31,7 @@ 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')
+xfailif_windows = pytest.mark.xfail(sys.platform == 'win32', reason='windows')
def run_opts(
@@ -54,13 +40,16 @@ def run_opts(
color=False,
verbose=False,
hook=None,
+ fail_fast=False,
remote_branch='',
local_branch='',
from_ref='',
to_ref='',
+ pre_rebase_upstream='',
+ pre_rebase_branch='',
remote_name='',
remote_url='',
- hook_stage='commit',
+ hook_stage='pre-commit',
show_diff_on_failure=False,
commit_msg_filename='',
prepare_commit_message_source='',
@@ -77,10 +66,13 @@ def run_opts(
color=color,
verbose=verbose,
hook=hook,
+ fail_fast=fail_fast,
remote_branch=remote_branch,
local_branch=local_branch,
from_ref=from_ref,
to_ref=to_ref,
+ pre_rebase_upstream=pre_rebase_upstream,
+ pre_rebase_branch=pre_rebase_branch,
remote_name=remote_name,
remote_url=remote_url,
hook_stage=hook_stage,
diff --git a/testing/zipapp/Dockerfile b/testing/zipapp/Dockerfile
index 7c74c1b2..ea967e38 100644
--- a/testing/zipapp/Dockerfile
+++ b/testing/zipapp/Dockerfile
@@ -1,4 +1,4 @@
-FROM ubuntu:focal
+FROM ubuntu:jammy
RUN : \
&& apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
@@ -11,4 +11,4 @@ RUN : \
ENV LANG=C.UTF-8 PATH=/venv/bin:$PATH
RUN : \
&& python3 -mvenv /venv \
- && pip install --no-cache-dir pip setuptools wheel no-manylinux --upgrade
+ && pip install --no-cache-dir pip distlib no-manylinux --upgrade
diff --git a/testing/zipapp/make b/testing/zipapp/make
index 37b5c355..43bb4373 100755
--- a/testing/zipapp/make
+++ b/testing/zipapp/make
@@ -4,7 +4,6 @@ from __future__ import annotations
import argparse
import base64
import hashlib
-import importlib.resources
import io
import os.path
import shutil
@@ -42,10 +41,17 @@ def _add_shim(dest: str) -> None:
with zipfile.ZipFile(bio, 'w') as zipf:
zipf.write(shim, arcname='__main__.py')
- with open(os.path.join(dest, 'python.exe'), 'wb') as f:
- f.write(importlib.resources.read_binary('distlib', 't32.exe'))
- f.write(b'#!py.exe -3\n')
- f.write(bio.getvalue())
+ with tempfile.TemporaryDirectory() as tmpdir:
+ _exit_if_retv(
+ 'podman', 'run', '--rm', '--volume', f'{tmpdir}:/out:rw', IMG,
+ 'cp', '/venv/lib/python3.10/site-packages/distlib/t32.exe', '/out',
+ )
+
+ with open(os.path.join(dest, 'python.exe'), 'wb') as f:
+ with open(os.path.join(tmpdir, 't32.exe'), 'rb') as t32:
+ f.write(t32.read())
+ f.write(b'#!py.exe -3\n')
+ f.write(bio.getvalue())
def _write_cache_key(version: str, wheeldir: str, dest: str) -> None:
@@ -101,9 +107,6 @@ def main() -> int:
shebang = '/usr/bin/env python3'
zipapp.create_archive(tmpdir, filename, interpreter=shebang)
- with open(f'{filename}.sha256sum', 'w') as f:
- subprocess.check_call(('sha256sum', filename), stdout=f)
-
return 0
diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py
index efb2aa84..2c42b80c 100644
--- a/tests/clientlib_test.py
+++ b/tests/clientlib_test.py
@@ -12,6 +12,9 @@ from pre_commit.clientlib import CONFIG_HOOK_DICT
from pre_commit.clientlib import CONFIG_REPO_DICT
from pre_commit.clientlib import CONFIG_SCHEMA
from pre_commit.clientlib import DEFAULT_LANGUAGE_VERSION
+from pre_commit.clientlib import InvalidManifestError
+from pre_commit.clientlib import load_manifest
+from pre_commit.clientlib import MANIFEST_HOOK_DICT
from pre_commit.clientlib import MANIFEST_SCHEMA
from pre_commit.clientlib import META_HOOK_DICT
from pre_commit.clientlib import OptionalSensibleRegexAtHook
@@ -39,56 +42,51 @@ def test_check_type_tag_success():
@pytest.mark.parametrize(
- ('config_obj', 'expected'), (
- (
- {
- 'repos': [{
- 'repo': 'git@github.com:pre-commit/pre-commit-hooks',
- 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37',
- 'hooks': [{'id': 'pyflakes', 'files': '\\.py$'}],
- }],
- },
- True,
- ),
- (
- {
- 'repos': [{
- 'repo': 'git@github.com:pre-commit/pre-commit-hooks',
- 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37',
- 'hooks': [
- {
- 'id': 'pyflakes',
- 'files': '\\.py$',
- 'args': ['foo', 'bar', 'baz'],
- },
- ],
- }],
- },
- True,
- ),
- (
- {
- 'repos': [{
- 'repo': 'git@github.com:pre-commit/pre-commit-hooks',
- 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37',
- 'hooks': [
- {
- 'id': 'pyflakes',
- 'files': '\\.py$',
- # Exclude pattern must be a string
- 'exclude': 0,
- 'args': ['foo', 'bar', 'baz'],
- },
- ],
- }],
- },
- False,
- ),
+ 'cfg',
+ (
+ {
+ 'repos': [{
+ 'repo': 'git@github.com:pre-commit/pre-commit-hooks',
+ 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37',
+ 'hooks': [{'id': 'pyflakes', 'files': '\\.py$'}],
+ }],
+ },
+ {
+ 'repos': [{
+ 'repo': 'git@github.com:pre-commit/pre-commit-hooks',
+ 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37',
+ 'hooks': [
+ {
+ 'id': 'pyflakes',
+ 'files': '\\.py$',
+ 'args': ['foo', 'bar', 'baz'],
+ },
+ ],
+ }],
+ },
),
)
-def test_config_valid(config_obj, expected):
- ret = is_valid_according_to_schema(config_obj, CONFIG_SCHEMA)
- assert ret is expected
+def test_config_valid(cfg):
+ assert is_valid_according_to_schema(cfg, CONFIG_SCHEMA)
+
+
+def test_invalid_config_wrong_type():
+ cfg = {
+ 'repos': [{
+ 'repo': 'git@github.com:pre-commit/pre-commit-hooks',
+ 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37',
+ 'hooks': [
+ {
+ 'id': 'pyflakes',
+ 'files': '\\.py$',
+ # Exclude pattern must be a string
+ 'exclude': 0,
+ 'args': ['foo', 'bar', 'baz'],
+ },
+ ],
+ }],
+ }
+ assert not is_valid_according_to_schema(cfg, CONFIG_SCHEMA)
def test_local_hooks_with_rev_fails():
@@ -197,14 +195,13 @@ def test_warn_mutable_rev_conditional():
),
)
def test_sensible_regex_validators_dont_pass_none(validator_cls):
- key = 'files'
+ validator = validator_cls('files', cfgv.check_string)
with pytest.raises(cfgv.ValidationError) as excinfo:
- validator = validator_cls(key, cfgv.check_string)
- validator.check({key: None})
+ validator.check({'files': None})
assert str(excinfo.value) == (
'\n'
- f'==> At key: {key}'
+ '==> At key: files'
'\n'
'=====> Expected string got NoneType'
)
@@ -261,6 +258,24 @@ def test_validate_optional_sensible_regex_at_local_hook(caplog):
]
+def test_validate_optional_sensible_regex_at_meta_hook(caplog):
+ config_obj = {
+ 'repo': 'meta',
+ 'hooks': [{'id': 'identity', 'files': 'dir/*.py'}],
+ }
+
+ cfgv.validate(config_obj, CONFIG_REPO_DICT)
+
+ assert caplog.record_tuples == [
+ (
+ 'pre_commit',
+ logging.WARNING,
+ "The 'files' field in hook 'identity' is a regex, not a glob "
+ "-- matching '/*' probably isn't what you want here",
+ ),
+ ]
+
+
@pytest.mark.parametrize(
('regex', 'warning'),
(
@@ -296,47 +311,128 @@ def test_validate_optional_sensible_regex_at_top_level(caplog, regex, warning):
assert caplog.record_tuples == [('pre_commit', logging.WARNING, warning)]
+def test_invalid_stages_error():
+ cfg = {'repos': [sample_local_config()]}
+ cfg['repos'][0]['hooks'][0]['stages'] = ['invalid']
+
+ with pytest.raises(cfgv.ValidationError) as excinfo:
+ cfgv.validate(cfg, CONFIG_SCHEMA)
+
+ assert str(excinfo.value) == (
+ '\n'
+ '==> At Config()\n'
+ '==> At key: repos\n'
+ "==> At Repository(repo='local')\n"
+ '==> At key: hooks\n'
+ "==> At Hook(id='do_not_commit')\n"
+ # this line was missing due to the custom validator
+ '==> At key: stages\n'
+ '==> At index 0\n'
+ "=====> Expected one of commit-msg, manual, post-checkout, post-commit, post-merge, post-rewrite, pre-commit, pre-merge-commit, pre-push, pre-rebase, prepare-commit-msg but got: 'invalid'" # noqa: E501
+ )
+
+
+def test_warning_for_deprecated_stages(caplog):
+ config_obj = sample_local_config()
+ config_obj['hooks'][0]['stages'] = ['commit', 'push']
+
+ cfgv.validate(config_obj, CONFIG_REPO_DICT)
+
+ assert caplog.record_tuples == [
+ (
+ 'pre_commit',
+ logging.WARNING,
+ 'hook id `do_not_commit` uses deprecated stage names '
+ '(commit, push) which will be removed in a future version. '
+ 'run: `pre-commit migrate-config` to automatically fix this.',
+ ),
+ ]
+
+
+def test_no_warning_for_non_deprecated_stages(caplog):
+ config_obj = sample_local_config()
+ config_obj['hooks'][0]['stages'] = ['pre-commit', 'pre-push']
+
+ cfgv.validate(config_obj, CONFIG_REPO_DICT)
+
+ assert caplog.record_tuples == []
+
+
+def test_warning_for_deprecated_default_stages(caplog):
+ cfg = {'default_stages': ['commit', 'push'], 'repos': []}
+
+ cfgv.validate(cfg, CONFIG_SCHEMA)
+
+ assert caplog.record_tuples == [
+ (
+ 'pre_commit',
+ logging.WARNING,
+ 'top-level `default_stages` uses deprecated stage names '
+ '(commit, push) which will be removed in a future version. '
+ 'run: `pre-commit migrate-config` to automatically fix this.',
+ ),
+ ]
+
+
+def test_no_warning_for_non_deprecated_default_stages(caplog):
+ cfg = {'default_stages': ['pre-commit', 'pre-push'], 'repos': []}
+
+ cfgv.validate(cfg, CONFIG_SCHEMA)
+
+ assert caplog.record_tuples == []
+
+
+def test_unsupported_language_migration():
+ cfg = {'repos': [sample_local_config(), sample_local_config()]}
+ cfg['repos'][0]['hooks'][0]['language'] = 'system'
+ cfg['repos'][1]['hooks'][0]['language'] = 'script'
+
+ cfgv.validate(cfg, CONFIG_SCHEMA)
+ ret = cfgv.apply_defaults(cfg, CONFIG_SCHEMA)
+
+ assert ret['repos'][0]['hooks'][0]['language'] == 'unsupported'
+ assert ret['repos'][1]['hooks'][0]['language'] == 'unsupported_script'
+
+
+def test_unsupported_language_migration_language_required():
+ cfg = {'repos': [sample_local_config()]}
+ del cfg['repos'][0]['hooks'][0]['language']
+
+ with pytest.raises(cfgv.ValidationError):
+ cfgv.validate(cfg, CONFIG_SCHEMA)
+
+
@pytest.mark.parametrize(
- ('manifest_obj', 'expected'),
+ 'manifest_obj',
(
- (
- [{
- 'id': 'a',
- 'name': 'b',
- 'entry': 'c',
- 'language': 'python',
- 'files': r'\.py$',
- }],
- True,
- ),
- (
- [{
- 'id': 'a',
- 'name': 'b',
- 'entry': 'c',
- 'language': 'python',
- 'language_version': 'python3.4',
- 'files': r'\.py$',
- }],
- True,
- ),
- (
- # A regression in 0.13.5: always_run and files are permissible
- [{
- 'id': 'a',
- 'name': 'b',
- 'entry': 'c',
- 'language': 'python',
- 'files': '',
- 'always_run': True,
- }],
- True,
- ),
+ [{
+ 'id': 'a',
+ 'name': 'b',
+ 'entry': 'c',
+ 'language': 'python',
+ 'files': r'\.py$',
+ }],
+ [{
+ 'id': 'a',
+ 'name': 'b',
+ 'entry': 'c',
+ 'language': 'python',
+ 'language_version': 'python3.4',
+ 'files': r'\.py$',
+ }],
+ # A regression in 0.13.5: always_run and files are permissible
+ [{
+ 'id': 'a',
+ 'name': 'b',
+ 'entry': 'c',
+ 'language': 'python',
+ 'files': '',
+ 'always_run': True,
+ }],
),
)
-def test_valid_manifests(manifest_obj, expected):
- ret = is_valid_according_to_schema(manifest_obj, MANIFEST_SCHEMA)
- assert ret is expected
+def test_valid_manifests(manifest_obj):
+ assert is_valid_according_to_schema(manifest_obj, MANIFEST_SCHEMA)
@pytest.mark.parametrize(
@@ -392,8 +488,39 @@ def test_parse_version():
def test_minimum_pre_commit_version_failing():
+ cfg = {'repos': [], 'minimum_pre_commit_version': '999'}
+ with pytest.raises(cfgv.ValidationError) as excinfo:
+ cfgv.validate(cfg, CONFIG_SCHEMA)
+ assert str(excinfo.value) == (
+ f'\n'
+ f'==> At Config()\n'
+ f'==> At key: minimum_pre_commit_version\n'
+ f'=====> pre-commit version 999 is required but version {C.VERSION} '
+ f'is installed. Perhaps run `pip install --upgrade pre-commit`.'
+ )
+
+
+def test_minimum_pre_commit_version_failing_in_config():
+ cfg = {'repos': [sample_local_config()]}
+ cfg['repos'][0]['hooks'][0]['minimum_pre_commit_version'] = '999'
+ with pytest.raises(cfgv.ValidationError) as excinfo:
+ cfgv.validate(cfg, CONFIG_SCHEMA)
+ assert str(excinfo.value) == (
+ f'\n'
+ f'==> At Config()\n'
+ f'==> At key: repos\n'
+ f"==> At Repository(repo='local')\n"
+ f'==> At key: hooks\n'
+ f"==> At Hook(id='do_not_commit')\n"
+ f'==> At key: minimum_pre_commit_version\n'
+ f'=====> pre-commit version 999 is required but version {C.VERSION} '
+ f'is installed. Perhaps run `pip install --upgrade pre-commit`.'
+ )
+
+
+def test_minimum_pre_commit_version_failing_before_other_error():
+ cfg = {'repos': 5, 'minimum_pre_commit_version': '999'}
with pytest.raises(cfgv.ValidationError) as excinfo:
- cfg = {'repos': [], 'minimum_pre_commit_version': '999'}
cfgv.validate(cfg, CONFIG_SCHEMA)
assert str(excinfo.value) == (
f'\n'
@@ -416,3 +543,65 @@ def test_warn_additional(schema):
x for x in schema.items if isinstance(x, cfgv.WarnAdditionalKeys)
)
assert allowed_keys == set(warn_additional.keys)
+
+
+def test_stages_migration_for_default_stages():
+ cfg = {
+ 'default_stages': ['commit-msg', 'push', 'commit', 'merge-commit'],
+ 'repos': [],
+ }
+ cfgv.validate(cfg, CONFIG_SCHEMA)
+ cfg = cfgv.apply_defaults(cfg, CONFIG_SCHEMA)
+ assert cfg['default_stages'] == [
+ 'commit-msg', 'pre-push', 'pre-commit', 'pre-merge-commit',
+ ]
+
+
+def test_manifest_stages_defaulting():
+ dct = {
+ 'id': 'fake-hook',
+ 'name': 'fake-hook',
+ 'entry': 'fake-hook',
+ 'language': 'system',
+ 'stages': ['commit-msg', 'push', 'commit', 'merge-commit'],
+ }
+ cfgv.validate(dct, MANIFEST_HOOK_DICT)
+ dct = cfgv.apply_defaults(dct, MANIFEST_HOOK_DICT)
+ assert dct['stages'] == [
+ 'commit-msg', 'pre-push', 'pre-commit', 'pre-merge-commit',
+ ]
+
+
+def test_config_hook_stages_defaulting_missing():
+ dct = {'id': 'fake-hook'}
+ cfgv.validate(dct, CONFIG_HOOK_DICT)
+ dct = cfgv.apply_defaults(dct, CONFIG_HOOK_DICT)
+ assert dct == {'id': 'fake-hook'}
+
+
+def test_config_hook_stages_defaulting():
+ dct = {
+ 'id': 'fake-hook',
+ 'stages': ['commit-msg', 'push', 'commit', 'merge-commit'],
+ }
+ cfgv.validate(dct, CONFIG_HOOK_DICT)
+ dct = cfgv.apply_defaults(dct, CONFIG_HOOK_DICT)
+ assert dct == {
+ 'id': 'fake-hook',
+ 'stages': ['commit-msg', 'pre-push', 'pre-commit', 'pre-merge-commit'],
+ }
+
+
+def test_manifest_v5_forward_compat(tmp_path):
+ manifest = tmp_path.joinpath('.pre-commit-hooks.yaml')
+ manifest.write_text('hooks: {}')
+
+ with pytest.raises(InvalidManifestError) as excinfo:
+ load_manifest(manifest)
+ assert str(excinfo.value) == (
+ f'\n'
+ f'==> File {manifest}\n'
+ f'=====> \n'
+ f'=====> pre-commit version 5 is required but version {C.VERSION} '
+ f'is installed. Perhaps run `pip install --upgrade pre-commit`.'
+ )
diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py
index 4bcb5d82..71bd0444 100644
--- a/tests/commands/autoupdate_test.py
+++ b/tests/commands/autoupdate_test.py
@@ -67,7 +67,7 @@ def test_rev_info_from_config():
def test_rev_info_update_up_to_date_repo(up_to_date):
config = make_config_from_repo(up_to_date)
- info = RevInfo.from_config(config)
+ info = RevInfo.from_config(config)._replace(hook_ids=frozenset(('foo',)))
new_info = info.update(tags_only=False, freeze=False)
assert info == new_info
@@ -139,7 +139,7 @@ def test_rev_info_update_does_not_freeze_if_already_sha(out_of_date):
assert new_info.frozen is None
-def test_autoupdate_up_to_date_repo(up_to_date, tmpdir, store):
+def test_autoupdate_up_to_date_repo(up_to_date, tmpdir):
contents = (
f'repos:\n'
f'- repo: {up_to_date}\n'
@@ -150,11 +150,11 @@ def test_autoupdate_up_to_date_repo(up_to_date, tmpdir, store):
cfg = tmpdir.join(C.CONFIG_FILE)
cfg.write(contents)
- assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0
+ assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0
assert cfg.read() == contents
-def test_autoupdate_old_revision_broken(tempdir_factory, in_tmpdir, store):
+def test_autoupdate_old_revision_broken(tempdir_factory, in_tmpdir):
"""In $FUTURE_VERSION, hooks.yaml will no longer be supported. This
asserts that when that day comes, pre-commit will be able to autoupdate
despite not being able to read hooks.yaml in that repository.
@@ -174,14 +174,14 @@ def test_autoupdate_old_revision_broken(tempdir_factory, in_tmpdir, store):
write_config('.', config)
with open(C.CONFIG_FILE) as f:
before = f.read()
- assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0
+ assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 0
with open(C.CONFIG_FILE) as f:
after = f.read()
assert before != after
assert update_rev in after
-def test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store):
+def test_autoupdate_out_of_date_repo(out_of_date, tmpdir):
fmt = (
'repos:\n'
'- repo: {}\n'
@@ -192,24 +192,24 @@ def test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store):
cfg = tmpdir.join(C.CONFIG_FILE)
cfg.write(fmt.format(out_of_date.path, out_of_date.original_rev))
- assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0
+ assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0
assert cfg.read() == fmt.format(out_of_date.path, out_of_date.head_rev)
-def test_autoupdate_with_core_useBuiltinFSMonitor(out_of_date, tmpdir, store):
+def test_autoupdate_with_core_useBuiltinFSMonitor(out_of_date, tmpdir):
# force the setting on "globally" for git
home = tmpdir.join('fakehome').ensure_dir()
home.join('.gitconfig').write('[core]\nuseBuiltinFSMonitor = true\n')
with envcontext.envcontext((('HOME', str(home)),)):
- test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store)
+ test_autoupdate_out_of_date_repo(out_of_date, tmpdir)
-def test_autoupdate_pure_yaml(out_of_date, tmpdir, store):
+def test_autoupdate_pure_yaml(out_of_date, tmpdir):
with mock.patch.object(yaml, 'Dumper', yaml.yaml.SafeDumper):
- test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store)
+ test_autoupdate_out_of_date_repo(out_of_date, tmpdir)
-def test_autoupdate_only_one_to_update(up_to_date, out_of_date, tmpdir, store):
+def test_autoupdate_only_one_to_update(up_to_date, out_of_date, tmpdir):
fmt = (
'repos:\n'
'- repo: {}\n'
@@ -228,7 +228,7 @@ def test_autoupdate_only_one_to_update(up_to_date, out_of_date, tmpdir, store):
)
cfg.write(before)
- assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0
+ assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0
assert cfg.read() == fmt.format(
up_to_date, git.head_rev(up_to_date),
out_of_date.path, out_of_date.head_rev,
@@ -236,7 +236,7 @@ def test_autoupdate_only_one_to_update(up_to_date, out_of_date, tmpdir, store):
def test_autoupdate_out_of_date_repo_with_correct_repo_name(
- out_of_date, in_tmpdir, store,
+ out_of_date, in_tmpdir,
):
stale_config = make_config_from_repo(
out_of_date.path, rev=out_of_date.original_rev, check=False,
@@ -249,7 +249,7 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name(
before = f.read()
repo_name = f'file://{out_of_date.path}'
ret = autoupdate(
- C.CONFIG_FILE, store, freeze=False, tags_only=False,
+ C.CONFIG_FILE, freeze=False, tags_only=False,
repos=(repo_name,),
)
with open(C.CONFIG_FILE) as f:
@@ -261,7 +261,7 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name(
def test_autoupdate_out_of_date_repo_with_wrong_repo_name(
- out_of_date, in_tmpdir, store,
+ out_of_date, in_tmpdir,
):
config = make_config_from_repo(
out_of_date.path, rev=out_of_date.original_rev, check=False,
@@ -272,7 +272,7 @@ def test_autoupdate_out_of_date_repo_with_wrong_repo_name(
before = f.read()
# It will not update it, because the name doesn't match
ret = autoupdate(
- C.CONFIG_FILE, store, freeze=False, tags_only=False,
+ C.CONFIG_FILE, freeze=False, tags_only=False,
repos=('dne',),
)
with open(C.CONFIG_FILE) as f:
@@ -281,7 +281,7 @@ def test_autoupdate_out_of_date_repo_with_wrong_repo_name(
assert before == after
-def test_does_not_reformat(tmpdir, out_of_date, store):
+def test_does_not_reformat(tmpdir, out_of_date):
fmt = (
'repos:\n'
'- repo: {}\n'
@@ -294,12 +294,12 @@ def test_does_not_reformat(tmpdir, out_of_date, store):
cfg = tmpdir.join(C.CONFIG_FILE)
cfg.write(fmt.format(out_of_date.path, out_of_date.original_rev))
- assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0
+ assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0
expected = fmt.format(out_of_date.path, out_of_date.head_rev)
assert cfg.read() == expected
-def test_does_not_change_mixed_endlines_read(up_to_date, tmpdir, store):
+def test_does_not_change_mixed_endlines_read(up_to_date, tmpdir):
fmt = (
'repos:\n'
'- repo: {}\n'
@@ -314,11 +314,11 @@ def test_does_not_change_mixed_endlines_read(up_to_date, tmpdir, store):
expected = fmt.format(up_to_date, git.head_rev(up_to_date)).encode()
cfg.write_binary(expected)
- assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0
+ assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0
assert cfg.read_binary() == expected
-def test_does_not_change_mixed_endlines_write(tmpdir, out_of_date, store):
+def test_does_not_change_mixed_endlines_write(tmpdir, out_of_date):
fmt = (
'repos:\n'
'- repo: {}\n'
@@ -333,12 +333,12 @@ def test_does_not_change_mixed_endlines_write(tmpdir, out_of_date, store):
fmt.format(out_of_date.path, out_of_date.original_rev).encode(),
)
- assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0
+ assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0
expected = fmt.format(out_of_date.path, out_of_date.head_rev).encode()
assert cfg.read_binary() == expected
-def test_loses_formatting_when_not_detectable(out_of_date, store, tmpdir):
+def test_loses_formatting_when_not_detectable(out_of_date, tmpdir):
"""A best-effort attempt is made at updating rev without rewriting
formatting. When the original formatting cannot be detected, this
is abandoned.
@@ -359,7 +359,7 @@ def test_loses_formatting_when_not_detectable(out_of_date, store, tmpdir):
cfg = tmpdir.join(C.CONFIG_FILE)
cfg.write(config)
- assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0
+ assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0
expected = (
f'repos:\n'
f'- repo: {out_of_date.path}\n'
@@ -370,43 +370,43 @@ def test_loses_formatting_when_not_detectable(out_of_date, store, tmpdir):
assert cfg.read() == expected
-def test_autoupdate_tagged_repo(tagged, in_tmpdir, store):
+def test_autoupdate_tagged_repo(tagged, in_tmpdir):
config = make_config_from_repo(tagged.path, rev=tagged.original_rev)
write_config('.', config)
- assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0
+ assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 0
with open(C.CONFIG_FILE) as f:
assert 'v1.2.3' in f.read()
-def test_autoupdate_freeze(tagged, in_tmpdir, store):
+def test_autoupdate_freeze(tagged, in_tmpdir):
config = make_config_from_repo(tagged.path, rev=tagged.original_rev)
write_config('.', config)
- assert autoupdate(C.CONFIG_FILE, store, freeze=True, tags_only=False) == 0
+ assert autoupdate(C.CONFIG_FILE, freeze=True, tags_only=False) == 0
with open(C.CONFIG_FILE) as f:
expected = f'rev: {tagged.head_rev} # frozen: v1.2.3'
assert expected in f.read()
# if we un-freeze it should remove the frozen comment
- assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0
+ assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 0
with open(C.CONFIG_FILE) as f:
assert 'rev: v1.2.3\n' in f.read()
-def test_autoupdate_tags_only(tagged, in_tmpdir, store):
+def test_autoupdate_tags_only(tagged, in_tmpdir):
# add some commits after the tag
git_commit(cwd=tagged.path)
config = make_config_from_repo(tagged.path, rev=tagged.original_rev)
write_config('.', config)
- assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=True) == 0
+ assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=True) == 0
with open(C.CONFIG_FILE) as f:
assert 'v1.2.3' in f.read()
-def test_autoupdate_latest_no_config(out_of_date, in_tmpdir, store):
+def test_autoupdate_latest_no_config(out_of_date, in_tmpdir):
config = make_config_from_repo(
out_of_date.path, rev=out_of_date.original_rev,
)
@@ -415,12 +415,12 @@ def test_autoupdate_latest_no_config(out_of_date, in_tmpdir, store):
cmd_output('git', 'rm', '-r', ':/', cwd=out_of_date.path)
git_commit(cwd=out_of_date.path)
- assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 1
+ assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 1
with open(C.CONFIG_FILE) as f:
assert out_of_date.original_rev in f.read()
-def test_hook_disppearing_repo_raises(hook_disappearing, store):
+def test_hook_disppearing_repo_raises(hook_disappearing):
config = make_config_from_repo(
hook_disappearing.path,
rev=hook_disappearing.original_rev,
@@ -428,10 +428,10 @@ def test_hook_disppearing_repo_raises(hook_disappearing, store):
)
info = RevInfo.from_config(config).update(tags_only=False, freeze=False)
with pytest.raises(RepositoryCannotBeUpdatedError):
- _check_hooks_still_exist_at_rev(config, info, store)
+ _check_hooks_still_exist_at_rev(config, info)
-def test_autoupdate_hook_disappearing_repo(hook_disappearing, tmpdir, store):
+def test_autoupdate_hook_disappearing_repo(hook_disappearing, tmpdir):
contents = (
f'repos:\n'
f'- repo: {hook_disappearing.path}\n'
@@ -442,21 +442,21 @@ def test_autoupdate_hook_disappearing_repo(hook_disappearing, tmpdir, store):
cfg = tmpdir.join(C.CONFIG_FILE)
cfg.write(contents)
- assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 1
+ assert autoupdate(str(cfg), freeze=False, tags_only=False) == 1
assert cfg.read() == contents
-def test_autoupdate_local_hooks(in_git_dir, store):
+def test_autoupdate_local_hooks(in_git_dir):
config = sample_local_config()
add_config_to_repo('.', config)
- assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0
+ assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 0
new_config_written = read_config('.')
assert len(new_config_written['repos']) == 1
assert new_config_written['repos'][0] == config
def test_autoupdate_local_hooks_with_out_of_date_repo(
- out_of_date, in_tmpdir, store,
+ out_of_date, in_tmpdir,
):
stale_config = make_config_from_repo(
out_of_date.path, rev=out_of_date.original_rev, check=False,
@@ -464,13 +464,13 @@ def test_autoupdate_local_hooks_with_out_of_date_repo(
local_config = sample_local_config()
config = {'repos': [local_config, stale_config]}
write_config('.', config)
- assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0
+ assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 0
new_config_written = read_config('.')
assert len(new_config_written['repos']) == 2
assert new_config_written['repos'][0] == local_config
-def test_autoupdate_meta_hooks(tmpdir, store):
+def test_autoupdate_meta_hooks(tmpdir):
cfg = tmpdir.join(C.CONFIG_FILE)
cfg.write(
'repos:\n'
@@ -478,7 +478,7 @@ def test_autoupdate_meta_hooks(tmpdir, store):
' hooks:\n'
' - id: check-useless-excludes\n',
)
- assert autoupdate(str(cfg), store, freeze=False, tags_only=True) == 0
+ assert autoupdate(str(cfg), freeze=False, tags_only=True) == 0
assert cfg.read() == (
'repos:\n'
'- repo: meta\n'
@@ -487,7 +487,7 @@ def test_autoupdate_meta_hooks(tmpdir, store):
)
-def test_updates_old_format_to_new_format(tmpdir, capsys, store):
+def test_updates_old_format_to_new_format(tmpdir, capsys):
cfg = tmpdir.join(C.CONFIG_FILE)
cfg.write(
'- repo: local\n'
@@ -497,7 +497,7 @@ def test_updates_old_format_to_new_format(tmpdir, capsys, store):
' entry: ./bin/foo.sh\n'
' language: script\n',
)
- assert autoupdate(str(cfg), store, freeze=False, tags_only=True) == 0
+ assert autoupdate(str(cfg), freeze=False, tags_only=True) == 0
contents = cfg.read()
assert contents == (
'repos:\n'
@@ -512,7 +512,7 @@ def test_updates_old_format_to_new_format(tmpdir, capsys, store):
assert out == 'Configuration has been migrated.\n'
-def test_maintains_rev_quoting_style(tmpdir, out_of_date, store):
+def test_maintains_rev_quoting_style(tmpdir, out_of_date):
fmt = (
'repos:\n'
'- repo: {path}\n'
@@ -527,6 +527,6 @@ def test_maintains_rev_quoting_style(tmpdir, out_of_date, store):
cfg = tmpdir.join(C.CONFIG_FILE)
cfg.write(fmt.format(path=out_of_date.path, rev=out_of_date.original_rev))
- assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0
+ assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0
expected = fmt.format(path=out_of_date.path, rev=out_of_date.head_rev)
assert cfg.read() == expected
diff --git a/tests/commands/gc_test.py b/tests/commands/gc_test.py
index c128e939..992b02f3 100644
--- a/tests/commands/gc_test.py
+++ b/tests/commands/gc_test.py
@@ -19,11 +19,13 @@ from testing.util import git_commit
def _repo_count(store):
- return len(store.select_all_repos())
+ with store.connect() as db:
+ return db.execute('SELECT COUNT(1) FROM repos').fetchone()[0]
def _config_count(store):
- return len(store.select_all_configs())
+ with store.connect() as db:
+ return db.execute('SELECT COUNT(1) FROM configs').fetchone()[0]
def _remove_config_assert_cleared(store, cap_out):
@@ -43,8 +45,9 @@ def test_gc(tempdir_factory, store, in_git_dir, cap_out):
store.mark_config_used(C.CONFIG_FILE)
# update will clone both the old and new repo, making the old one gc-able
- install_hooks(C.CONFIG_FILE, store)
- assert not autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False)
+ assert not install_hooks(C.CONFIG_FILE, store)
+ assert not autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False)
+ assert not install_hooks(C.CONFIG_FILE, store)
assert _config_count(store) == 1
assert _repo_count(store) == 2
@@ -152,7 +155,8 @@ def test_invalid_manifest_gcd(tempdir_factory, store, in_git_dir, cap_out):
install_hooks(C.CONFIG_FILE, store)
# we'll "break" the manifest to simulate an old version clone
- (_, _, path), = store.select_all_repos()
+ with store.connect() as db:
+ path, = db.execute('SELECT path FROM repos').fetchone()
os.remove(os.path.join(path, C.MANIFEST_FILE))
assert _config_count(store) == 1
@@ -161,3 +165,11 @@ def test_invalid_manifest_gcd(tempdir_factory, store, in_git_dir, cap_out):
assert _config_count(store) == 1
assert _repo_count(store) == 0
assert cap_out.get().splitlines()[-1] == '1 repo(s) removed.'
+
+
+def test_gc_pre_1_14_roll_forward(store, cap_out):
+ with store.connect() as db: # simulate pre-1.14.0
+ db.executescript('DROP TABLE configs')
+
+ assert not gc(store)
+ assert cap_out.get() == '0 repo(s) removed.\n'
diff --git a/tests/commands/hazmat_test.py b/tests/commands/hazmat_test.py
new file mode 100644
index 00000000..df957e36
--- /dev/null
+++ b/tests/commands/hazmat_test.py
@@ -0,0 +1,99 @@
+from __future__ import annotations
+
+import sys
+
+import pytest
+
+from pre_commit.commands.hazmat import _cmd_filenames
+from pre_commit.commands.hazmat import main
+from testing.util import cwd
+
+
+def test_cmd_filenames_no_dash_dash():
+ with pytest.raises(SystemExit) as excinfo:
+ _cmd_filenames(('no', 'dashdash', 'here'))
+ msg, = excinfo.value.args
+ assert msg == 'hazmat entry must end with `--`'
+
+
+def test_cmd_filenames_no_filenames():
+ cmd, filenames = _cmd_filenames(('hello', 'world', '--'))
+ assert cmd == ('hello', 'world')
+ assert filenames == ()
+
+
+def test_cmd_filenames_some_filenames():
+ cmd, filenames = _cmd_filenames(('hello', 'world', '--', 'f1', 'f2'))
+ assert cmd == ('hello', 'world')
+ assert filenames == ('f1', 'f2')
+
+
+def test_cmd_filenames_multiple_dashdash():
+ cmd, filenames = _cmd_filenames(('hello', '--', 'arg', '--', 'f1', 'f2'))
+ assert cmd == ('hello', '--', 'arg')
+ assert filenames == ('f1', 'f2')
+
+
+def test_cd_unexpected_filename():
+ with pytest.raises(SystemExit) as excinfo:
+ main(('cd', 'subdir', 'cmd', '--', 'subdir/1', 'not-subdir/2'))
+ msg, = excinfo.value.args
+ assert msg == "unexpected file without prefix='subdir/': not-subdir/2"
+
+
+def _norm(out):
+ return out.replace('\r\n', '\n')
+
+
+def test_cd(tmp_path, capfd):
+ subdir = tmp_path.joinpath('subdir')
+ subdir.mkdir()
+ subdir.joinpath('a').write_text('a')
+ subdir.joinpath('b').write_text('b')
+
+ with cwd(tmp_path):
+ ret = main((
+ 'cd', 'subdir',
+ sys.executable, '-c',
+ 'import os; print(os.getcwd());'
+ 'import sys; [print(open(f).read()) for f in sys.argv[1:]]',
+ '--',
+ 'subdir/a', 'subdir/b',
+ ))
+
+ assert ret == 0
+ out, err = capfd.readouterr()
+ assert _norm(out) == f'{subdir}\na\nb\n'
+ assert err == ''
+
+
+def test_ignore_exit_code(capfd):
+ ret = main((
+ 'ignore-exit-code', sys.executable, '-c', 'raise SystemExit("bye")',
+ ))
+ assert ret == 0
+ out, err = capfd.readouterr()
+ assert out == ''
+ assert _norm(err) == 'bye\n'
+
+
+def test_n1(capfd):
+ ret = main((
+ 'n1', sys.executable, '-c', 'import sys; print(sys.argv[1:])',
+ '--',
+ 'foo', 'bar', 'baz',
+ ))
+ assert ret == 0
+ out, err = capfd.readouterr()
+ assert _norm(out) == "['foo']\n['bar']\n['baz']\n"
+ assert err == ''
+
+
+def test_n1_some_error_code():
+ ret = main((
+ 'n1', sys.executable, '-c',
+ 'import sys; raise SystemExit(sys.argv[1] == "error")',
+ '--',
+ 'ok', 'error', 'ok',
+ ))
+ assert ret == 1
diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py
index aa321dab..d757e85c 100644
--- a/tests/commands/hook_impl_test.py
+++ b/tests/commands/hook_impl_test.py
@@ -100,6 +100,8 @@ def test_run_legacy_recursive(tmpdir):
('commit-msg', ['.git/COMMIT_EDITMSG']),
('post-commit', []),
('post-merge', ['1']),
+ ('pre-rebase', ['main', 'topic']),
+ ('pre-rebase', ['main']),
('post-checkout', ['old_head', 'new_head', '1']),
('post-rewrite', ['amend']),
# multiple choices for commit-editmsg
@@ -139,13 +141,36 @@ def test_check_args_length_prepare_commit_msg_error():
)
+def test_check_args_length_pre_rebase_error():
+ with pytest.raises(SystemExit) as excinfo:
+ hook_impl._check_args_length('pre-rebase', [])
+ msg, = excinfo.value.args
+ assert msg == 'hook-impl for pre-rebase expected 1 or 2 arguments but got 0: []' # noqa: E501
+
+
def test_run_ns_pre_commit():
ns = hook_impl._run_ns('pre-commit', True, (), b'')
assert ns is not None
- assert ns.hook_stage == 'commit'
+ assert ns.hook_stage == 'pre-commit'
assert ns.color is True
+def test_run_ns_pre_rebase():
+ ns = hook_impl._run_ns('pre-rebase', True, ('main', 'topic'), b'')
+ assert ns is not None
+ assert ns.hook_stage == 'pre-rebase'
+ assert ns.color is True
+ assert ns.pre_rebase_upstream == 'main'
+ assert ns.pre_rebase_branch == 'topic'
+
+ ns = hook_impl._run_ns('pre-rebase', True, ('main',), b'')
+ assert ns is not None
+ assert ns.hook_stage == 'pre-rebase'
+ assert ns.color is True
+ assert ns.pre_rebase_upstream == 'main'
+ assert ns.pre_rebase_branch is None
+
+
def test_run_ns_commit_msg():
ns = hook_impl._run_ns('commit-msg', False, ('.git/COMMIT_MSG',), b'')
assert ns is not None
@@ -245,7 +270,7 @@ def test_run_ns_pre_push_updating_branch(push_example):
ns = hook_impl._run_ns('pre-push', False, args, stdin)
assert ns is not None
- assert ns.hook_stage == 'push'
+ assert ns.hook_stage == 'pre-push'
assert ns.color is False
assert ns.remote_name == 'origin'
assert ns.remote_url == src
diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py
index a1ecda86..9eb0e741 100644
--- a/tests/commands/install_uninstall_test.py
+++ b/tests/commands/install_uninstall_test.py
@@ -349,8 +349,9 @@ def test_install_existing_hooks_no_overwrite(tempdir_factory, store):
# We should run both the legacy and pre-commit hooks
ret, output = _get_commit_output(tempdir_factory)
assert ret == 0
- assert output.startswith('legacy hook\n')
- NORMAL_PRE_COMMIT_RUN.assert_matches(output[len('legacy hook\n'):])
+ legacy = 'legacy hook\n'
+ assert output.startswith(legacy)
+ NORMAL_PRE_COMMIT_RUN.assert_matches(output.removeprefix(legacy))
def test_legacy_overwriting_legacy_hook(tempdir_factory, store):
@@ -375,8 +376,9 @@ def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory, store):
# We should run both the legacy and pre-commit hooks
ret, output = _get_commit_output(tempdir_factory)
assert ret == 0
- assert output.startswith('legacy hook\n')
- NORMAL_PRE_COMMIT_RUN.assert_matches(output[len('legacy hook\n'):])
+ legacy = 'legacy hook\n'
+ assert output.startswith(legacy)
+ NORMAL_PRE_COMMIT_RUN.assert_matches(output.removeprefix(legacy))
def test_install_with_existing_non_utf8_script(tmpdir, store):
@@ -810,6 +812,46 @@ def test_post_merge_integration(tempdir_factory, store):
assert os.path.exists('post-merge.tmp')
+def test_pre_rebase_integration(tempdir_factory, store):
+ path = git_dir(tempdir_factory)
+ config = {
+ 'repos': [
+ {
+ 'repo': 'local',
+ 'hooks': [{
+ 'id': 'pre-rebase',
+ 'name': 'Pre rebase',
+ 'entry': 'touch pre-rebase.tmp',
+ 'language': 'system',
+ 'always_run': True,
+ 'verbose': True,
+ 'stages': ['pre-rebase'],
+ }],
+ },
+ ],
+ }
+ write_config(path, config)
+ with cwd(path):
+ install(C.CONFIG_FILE, store, hook_types=['pre-rebase'])
+ open('foo', 'a').close()
+ cmd_output('git', 'add', '.')
+ git_commit()
+
+ cmd_output('git', 'checkout', '-b', 'branch')
+ open('bar', 'a').close()
+ cmd_output('git', 'add', '.')
+ git_commit()
+
+ cmd_output('git', 'checkout', 'master')
+ open('baz', 'a').close()
+ cmd_output('git', 'add', '.')
+ git_commit()
+
+ cmd_output('git', 'checkout', 'branch')
+ cmd_output('git', 'rebase', 'master', 'branch')
+ assert os.path.exists('pre-rebase.tmp')
+
+
def test_post_rewrite_integration(tempdir_factory, store):
path = git_dir(tempdir_factory)
config = {
diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py
index fca1ad92..a517d2f4 100644
--- a/tests/commands/migrate_config_test.py
+++ b/tests/commands/migrate_config_test.py
@@ -1,10 +1,26 @@
from __future__ import annotations
+from unittest import mock
+
import pytest
+import yaml
import pre_commit.constants as C
from pre_commit.clientlib import InvalidConfigError
from pre_commit.commands.migrate_config import migrate_config
+from pre_commit.yaml import yaml_compose
+
+
+@pytest.fixture(autouse=True, params=['c', 'pure'])
+def switch_pyyaml_impl(request):
+ if request.param == 'c':
+ yield
+ else:
+ with mock.patch.dict(
+ yaml_compose.keywords,
+ {'Loader': yaml.SafeLoader},
+ ):
+ yield
def test_migrate_config_normal_format(tmpdir, capsys):
@@ -134,6 +150,127 @@ def test_migrate_config_sha_to_rev(tmpdir):
)
+def test_migrate_config_sha_to_rev_json(tmp_path):
+ contents = """\
+{"repos": [{
+ "repo": "https://github.com/pre-commit/pre-commit-hooks",
+ "sha": "v1.2.0",
+ "hooks": []
+}]}
+"""
+ expected = """\
+{"repos": [{
+ "repo": "https://github.com/pre-commit/pre-commit-hooks",
+ "rev": "v1.2.0",
+ "hooks": []
+}]}
+"""
+ cfg = tmp_path.joinpath('cfg.yaml')
+ cfg.write_text(contents)
+ assert not migrate_config(str(cfg))
+ assert cfg.read_text() == expected
+
+
+def test_migrate_config_language_python_venv(tmp_path):
+ src = '''\
+repos:
+- 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_quoted_python_venv(tmp_path):
+ src = '''\
+repos:
+- repo: local
+ hooks:
+ - id: example
+ name: example
+ entry: example
+ language: "python_venv"
+'''
+ expected = '''\
+repos:
+- repo: local
+ hooks:
+ - id: example
+ name: example
+ entry: example
+ language: "python"
+'''
+ cfg = tmp_path.joinpath('cfg.yaml')
+ cfg.write_text(src)
+ assert migrate_config(str(cfg)) == 0
+ assert cfg.read_text() == expected
+
+
+def test_migrate_config_default_stages(tmp_path):
+ src = '''\
+default_stages: [commit, push, merge-commit, commit-msg]
+repos: []
+'''
+ expected = '''\
+default_stages: [pre-commit, pre-push, pre-merge-commit, commit-msg]
+repos: []
+'''
+ cfg = tmp_path.joinpath('cfg.yaml')
+ cfg.write_text(src)
+ assert migrate_config(str(cfg)) == 0
+ assert cfg.read_text() == expected
+
+
+def test_migrate_config_hook_stages(tmp_path):
+ src = '''\
+repos:
+- repo: local
+ hooks:
+ - id: example
+ name: example
+ entry: example
+ language: system
+ stages: ["commit", "push", "merge-commit", "commit-msg"]
+'''
+ expected = '''\
+repos:
+- repo: local
+ hooks:
+ - id: example
+ name: example
+ entry: example
+ language: system
+ stages: ["pre-commit", "pre-push", "pre-merge-commit", "commit-msg"]
+'''
+ cfg = tmp_path.joinpath('cfg.yaml')
+ cfg.write_text(src)
+ assert migrate_config(str(cfg)) == 0
+ assert cfg.read_text() == expected
+
+
def test_migrate_config_invalid_yaml(tmpdir):
contents = '['
cfg = tmpdir.join(C.CONFIG_FILE)
diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py
index 03d741e0..e4af1e16 100644
--- a/tests/commands/run_test.py
+++ b/tests/commands/run_test.py
@@ -4,7 +4,7 @@ import os.path
import shlex
import sys
import time
-from typing import MutableMapping
+from collections.abc import MutableMapping
from unittest import mock
import pytest
@@ -293,7 +293,7 @@ def test_verbose_duration(cap_out, store, in_git_dir, t1, t2, expected):
write_config('.', {'repo': 'meta', 'hooks': [{'id': 'identity'}]})
cmd_output('git', 'add', '.')
opts = run_opts(verbose=True)
- with mock.patch.object(time, 'time', side_effect=(t1, t2)):
+ with mock.patch.object(time, 'monotonic', side_effect=(t1, t2)):
ret, printed = _do_run(cap_out, store, str(in_git_dir), opts)
assert ret == 0
assert expected in printed
@@ -354,13 +354,13 @@ def test_show_diff_on_failure(
({'hook': 'bash_hook'}, (b'Bash hook', b'Passed'), 0, True),
(
{'hook': 'nope'},
- (b'No hook with id `nope` in stage `commit`',),
+ (b'No hook with id `nope` in stage `pre-commit`',),
1,
True,
),
(
- {'hook': 'nope', 'hook_stage': 'push'},
- (b'No hook with id `nope` in stage `push`',),
+ {'hook': 'nope', 'hook_stage': 'pre-push'},
+ (b'No hook with id `nope` in stage `pre-push`',),
1,
True,
),
@@ -563,6 +563,16 @@ def test_merge_conflict_resolved(cap_out, store, in_merge_conflict):
assert msg in printed
+def test_rebase(cap_out, store, repo_with_passing_hook):
+ args = run_opts(pre_rebase_upstream='master', pre_rebase_branch='topic')
+ environ: MutableMapping[str, str] = {}
+ ret, printed = _do_run(
+ cap_out, store, repo_with_passing_hook, args, environ,
+ )
+ assert environ['PRE_COMMIT_PRE_REBASE_UPSTREAM'] == 'master'
+ assert environ['PRE_COMMIT_PRE_REBASE_BRANCH'] == 'topic'
+
+
@pytest.mark.parametrize(
('hooks', 'expected'),
(
@@ -766,6 +776,47 @@ def test_lots_of_files(store, tempdir_factory):
)
+def test_no_textconv(cap_out, store, repo_with_passing_hook):
+ # git textconv filters can hide changes from hooks
+ with open('.gitattributes', 'w') as fp:
+ fp.write('*.jpeg diff=empty\n')
+
+ with open('.git/config', 'a') as fp:
+ fp.write('[diff "empty"]\n')
+ fp.write('textconv = "true"\n')
+
+ config = {
+ 'repo': 'local',
+ 'hooks': [
+ {
+ 'id': 'extend-jpeg',
+ 'name': 'extend-jpeg',
+ 'language': 'system',
+ 'entry': (
+ f'{shlex.quote(sys.executable)} -c "import sys; '
+ 'open(sys.argv[1], \'ab\').write(b\'\\x00\')"'
+ ),
+ 'types': ['jpeg'],
+ },
+ ],
+ }
+ add_config_to_repo(repo_with_passing_hook, config)
+
+ stage_a_file('example.jpeg')
+
+ _test_run(
+ cap_out,
+ store,
+ repo_with_passing_hook,
+ {},
+ (
+ b'Failed',
+ ),
+ expected_ret=1,
+ stage=False,
+ )
+
+
def test_stages(cap_out, store, repo_with_passing_hook):
config = {
'repo': 'local',
@@ -777,7 +828,7 @@ def test_stages(cap_out, store, repo_with_passing_hook):
'language': 'pygrep',
'stages': [stage],
}
- for i, stage in enumerate(('commit', 'push', 'manual'), 1)
+ for i, stage in enumerate(('pre-commit', 'pre-push', 'manual'), 1)
],
}
add_config_to_repo(repo_with_passing_hook, config)
@@ -792,8 +843,8 @@ def test_stages(cap_out, store, repo_with_passing_hook):
assert printed.count(b'hook ') == 1
return printed
- assert _run_for_stage('commit').startswith(b'hook 1...')
- assert _run_for_stage('push').startswith(b'hook 2...')
+ assert _run_for_stage('pre-commit').startswith(b'hook 1...')
+ assert _run_for_stage('pre-push').startswith(b'hook 2...')
assert _run_for_stage('manual').startswith(b'hook 3...')
@@ -1037,6 +1088,35 @@ def test_fail_fast_per_hook(cap_out, store, repo_with_failing_hook):
assert printed.count(b'Failing hook') == 1
+def test_fail_fast_not_prev_failures(cap_out, store, repo_with_failing_hook):
+ with modify_config() as config:
+ config['repos'].append({
+ 'repo': 'meta',
+ 'hooks': [
+ {'id': 'identity', 'fail_fast': True},
+ {'id': 'identity', 'name': 'run me!'},
+ ],
+ })
+ stage_a_file()
+
+ ret, printed = _do_run(cap_out, store, repo_with_failing_hook, run_opts())
+ # should still run the last hook since the `fail_fast` one didn't fail
+ assert printed.count(b'run me!') == 1
+
+
+def test_fail_fast_run_arg(cap_out, store, repo_with_failing_hook):
+ with modify_config() as config:
+ # More than one hook to demonstrate early exit
+ config['repos'][0]['hooks'] *= 2
+ stage_a_file()
+
+ ret, printed = _do_run(
+ cap_out, store, repo_with_failing_hook, run_opts(fail_fast=True),
+ )
+ # it should have only run one hook due to the CLI flag
+ assert printed.count(b'Failing hook') == 1
+
+
def test_classifier_removes_dne():
classifier = Classifier(('this_file_does_not_exist',))
assert classifier.filenames == []
@@ -1076,8 +1156,8 @@ def test_classifier_empty_types_or(tmpdir):
types_or=[],
exclude_types=[],
)
- assert for_symlink == ['foo']
- assert for_file == ['bar']
+ assert tuple(for_symlink) == ('foo',)
+ assert tuple(for_file) == ('bar',)
@pytest.fixture
@@ -1091,33 +1171,33 @@ def some_filenames():
def test_include_exclude_base_case(some_filenames):
ret = filter_by_include_exclude(some_filenames, '', '^$')
- assert ret == [
+ assert tuple(ret) == (
'.pre-commit-hooks.yaml',
'pre_commit/git.py',
'pre_commit/main.py',
- ]
+ )
def test_matches_broken_symlink(tmpdir):
with tmpdir.as_cwd():
os.symlink('does-not-exist', 'link')
ret = filter_by_include_exclude({'link'}, '', '^$')
- assert ret == ['link']
+ assert tuple(ret) == ('link',)
def test_include_exclude_total_match(some_filenames):
ret = filter_by_include_exclude(some_filenames, r'^.*\.py$', '^$')
- assert ret == ['pre_commit/git.py', 'pre_commit/main.py']
+ assert tuple(ret) == ('pre_commit/git.py', 'pre_commit/main.py')
def test_include_exclude_does_search_instead_of_match(some_filenames):
ret = filter_by_include_exclude(some_filenames, r'\.yaml$', '^$')
- assert ret == ['.pre-commit-hooks.yaml']
+ assert tuple(ret) == ('.pre-commit-hooks.yaml',)
def test_include_exclude_exclude_removes_files(some_filenames):
ret = filter_by_include_exclude(some_filenames, '', r'\.py$')
- assert ret == ['.pre-commit-hooks.yaml']
+ assert tuple(ret) == ('.pre-commit-hooks.yaml',)
def test_args_hook_only(cap_out, store, repo_with_passing_hook):
@@ -1132,7 +1212,7 @@ def test_args_hook_only(cap_out, store, repo_with_passing_hook):
),
'language': 'system',
'files': r'\.py$',
- 'stages': ['commit'],
+ 'stages': ['pre-commit'],
},
{
'id': 'do_not_commit',
diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py
index 0b2db7e5..c5f891ea 100644
--- a/tests/commands/try_repo_test.py
+++ b/tests/commands/try_repo_test.py
@@ -43,7 +43,7 @@ def _run_try_repo(tempdir_factory, **kwargs):
def test_try_repo_repo_only(cap_out, tempdir_factory):
- with mock.patch.object(time, 'time', return_value=0.0):
+ with mock.patch.object(time, 'monotonic', return_value=0.0):
_run_try_repo(tempdir_factory, verbose=True)
start, config, rest = _get_out(cap_out)
assert start == ''
diff --git a/tests/conftest.py b/tests/conftest.py
index 30761715..8c9cd14d 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -2,7 +2,6 @@ from __future__ import annotations
import functools
import io
-import logging
import os.path
from unittest import mock
@@ -203,42 +202,25 @@ def store(tempdir_factory):
yield Store(os.path.join(tempdir_factory.get(), '.pre-commit'))
-@pytest.fixture
-def log_info_mock():
- with mock.patch.object(logging.getLogger('pre_commit'), 'info') as mck:
- yield mck
-
-
-class FakeStream:
- def __init__(self):
- self.data = io.BytesIO()
-
- def write(self, s):
- self.data.write(s)
-
- def flush(self):
- pass
-
-
class Fixture:
- def __init__(self, stream):
+ def __init__(self, stream: io.BytesIO) -> None:
self._stream = stream
- def get_bytes(self):
+ def get_bytes(self) -> bytes:
"""Get the output as-if no encoding occurred"""
- data = self._stream.data.getvalue()
- self._stream.data.seek(0)
- self._stream.data.truncate()
+ data = self._stream.getvalue()
+ self._stream.seek(0)
+ self._stream.truncate()
return data.replace(b'\r\n', b'\n')
- def get(self):
+ def get(self) -> str:
"""Get the output assuming it was written as UTF-8 bytes"""
return self.get_bytes().decode()
@pytest.fixture
def cap_out():
- stream = FakeStream()
+ stream = io.BytesIO()
write = functools.partial(output.write, stream=stream)
write_line_b = functools.partial(output.write_line_b, stream=stream)
with mock.patch.multiple(output, write=write, write_line_b=write_line_b):
diff --git a/tests/git_test.py b/tests/git_test.py
index 93f5a1c6..02b6ce3a 100644
--- a/tests/git_test.py
+++ b/tests/git_test.py
@@ -141,6 +141,15 @@ def test_get_conflicted_files_unstaged_files(in_merge_conflict):
assert ret == {'conflict_file'}
+def test_get_conflicted_files_with_file_named_head(in_merge_conflict):
+ resolve_conflict()
+ open('HEAD', 'w').close()
+ cmd_output('git', 'add', 'HEAD')
+
+ ret = set(git.get_conflicted_files())
+ assert ret == {'conflict_file', 'HEAD'}
+
+
MERGE_MSG = b"Merge branch 'foo' into bar\n\nConflicts:\n\tconflict_file\n"
OTHER_MERGE_MSG = MERGE_MSG + b'\tother_conflict_file\n'
diff --git a/tests/languages/helpers_test.py b/tests/lang_base_test.py
similarity index 50%
rename from tests/languages/helpers_test.py
rename to tests/lang_base_test.py
index c209e7e6..9fac83da 100644
--- a/tests/languages/helpers_test.py
+++ b/tests/lang_base_test.py
@@ -1,6 +1,5 @@
from __future__ import annotations
-import multiprocessing
import os.path
import sys
from unittest import mock
@@ -8,8 +7,9 @@ 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.languages import helpers
+from pre_commit import xargs
from pre_commit.prefix import Prefix
from pre_commit.util import CalledProcessError
@@ -32,42 +32,42 @@ def homedir_mck():
def test_exe_exists_does_not_exist(find_exe_mck, homedir_mck):
find_exe_mck.return_value = None
- assert helpers.exe_exists('ruby') is False
+ assert lang_base.exe_exists('ruby') is False
def test_exe_exists_exists(find_exe_mck, homedir_mck):
find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby')
- assert helpers.exe_exists('ruby') is True
+ assert lang_base.exe_exists('ruby') is True
def test_exe_exists_false_if_shim(find_exe_mck, homedir_mck):
find_exe_mck.return_value = os.path.normpath('/foo/shims/ruby')
- assert helpers.exe_exists('ruby') is False
+ assert lang_base.exe_exists('ruby') is False
def test_exe_exists_false_if_homedir(find_exe_mck, homedir_mck):
find_exe_mck.return_value = os.path.normpath('/home/me/somedir/ruby')
- assert helpers.exe_exists('ruby') is False
+ assert lang_base.exe_exists('ruby') is False
def test_exe_exists_commonpath_raises_ValueError(find_exe_mck, homedir_mck):
find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby')
with mock.patch.object(os.path, 'commonpath', side_effect=ValueError):
- assert helpers.exe_exists('ruby') is True
+ assert lang_base.exe_exists('ruby') is True
def test_exe_exists_true_when_homedir_is_slash(find_exe_mck):
find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby')
with mock.patch.object(os.path, 'expanduser', return_value=os.sep):
- assert helpers.exe_exists('ruby') is True
+ assert lang_base.exe_exists('ruby') is True
def test_basic_get_default_version():
- assert helpers.basic_get_default_version() == C.DEFAULT
+ assert lang_base.basic_get_default_version() == C.DEFAULT
def test_basic_health_check():
- assert helpers.basic_health_check(Prefix('.'), 'default') is None
+ assert lang_base.basic_health_check(Prefix('.'), 'default') is None
def test_failed_setup_command_does_not_unicode_error():
@@ -79,12 +79,27 @@ def test_failed_setup_command_does_not_unicode_error():
# an assertion that this does not raise `UnicodeError`
with pytest.raises(CalledProcessError):
- helpers.run_setup_cmd(Prefix('.'), (sys.executable, '-c', script))
+ lang_base.setup_cmd(Prefix('.'), (sys.executable, '-c', script))
+
+
+def test_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:
- helpers.assert_no_additional_deps('lang', ['hmmm'])
+ lang_base.assert_no_additional_deps('lang', ['hmmm'])
msg, = excinfo.value.args
assert msg == (
'for now, pre-commit does not support additional_dependencies for '
@@ -93,43 +108,71 @@ def test_assert_no_additional_deps():
)
-def test_target_concurrency_normal():
- with mock.patch.object(multiprocessing, 'cpu_count', return_value=123):
- with mock.patch.dict(os.environ, {}, clear=True):
- assert helpers.target_concurrency() == 123
+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_testing_env_var():
- with mock.patch.dict(
- os.environ, {'PRE_COMMIT_NO_CONCURRENCY': '1'}, clear=True,
- ):
- assert helpers.target_concurrency() == 1
+@pytest.fixture
+def cpu_count_mck():
+ with mock.patch.object(xargs, 'cpu_count', return_value=4):
+ yield
-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
+@pytest.mark.parametrize(
+ ('var', 'expected'),
+ (
+ ('PRE_COMMIT_NO_CONCURRENCY', 1),
+ ('TRAVIS', 2),
+ (None, 4),
+ ),
+)
+def test_target_concurrency(cpu_count_mck, var, expected):
+ with mock.patch.dict(os.environ, {var: '1'} if var else {}, clear=True):
+ assert lang_base.target_concurrency() == expected
def test_shuffled_is_deterministic():
seq = [str(i) for i in range(10)]
expected = ['4', '0', '5', '1', '8', '6', '2', '3', '7', '9']
- assert helpers._shuffled(seq) == expected
+ assert lang_base._shuffled(seq) == expected
def test_xargs_require_serial_is_not_shuffled():
- ret, out = helpers.run_xargs(
+ ret, out = lang_base.run_xargs(
('echo',), [str(i) for i in range(10)],
require_serial=True,
color=False,
)
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'
+
+
+def test_hook_cmd():
+ assert lang_base.hook_cmd('echo hi', ()) == ('echo', 'hi')
+
+
+def test_hook_cmd_hazmat():
+ ret = lang_base.hook_cmd('pre-commit hazmat cd a echo -- b', ())
+ assert ret == (
+ sys.executable, '-m', 'pre_commit.commands.hazmat',
+ 'cd', 'a', 'echo', '--', 'b',
+ )
diff --git a/tests/languages/dart_test.py b/tests/languages/dart_test.py
index 5bb5aa68..213d888e 100644
--- a/tests/languages/dart_test.py
+++ b/tests/languages/dart_test.py
@@ -10,7 +10,7 @@ from testing.language_helpers import run_language
def test_dart(tmp_path):
pubspec_yaml = '''\
environment:
- sdk: '>=2.10.0 <3.0.0'
+ sdk: '>=2.12.0 <4.0.0'
name: hello_world_dart
diff --git a/tests/languages/docker_image_test.py b/tests/languages/docker_image_test.py
new file mode 100644
index 00000000..4f720600
--- /dev/null
+++ b/tests/languages/docker_image_test.py
@@ -0,0 +1,59 @@
+from __future__ import annotations
+
+import pytest
+
+from pre_commit.languages import docker_image
+from pre_commit.util import cmd_output_b
+from testing.language_helpers import run_language
+from testing.util import xfailif_windows
+
+
+@pytest.fixture(autouse=True, scope='module')
+def _ensure_image_available():
+ cmd_output_b('docker', 'run', '--rm', 'ubuntu:22.04', 'echo')
+
+
+@xfailif_windows # pragma: win32 no cover
+def test_docker_image_hook_via_entrypoint(tmp_path):
+ ret = run_language(
+ 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')
+
+
+@xfailif_windows # pragma: win32 no cover
+def test_docker_image_color_tty(tmp_path):
+ ret = run_language(
+ tmp_path,
+ docker_image,
+ 'ubuntu:22.04',
+ args=('grep', '--color', 'root', '/etc/group'),
+ color=True,
+ )
+ assert ret == (0, b'\x1b[01;31m\x1b[Kroot\x1b[m\x1b[K:x:0:\n')
+
+
+@xfailif_windows # pragma: win32 no cover
+def test_docker_image_no_color_no_tty(tmp_path):
+ ret = run_language(
+ tmp_path,
+ docker_image,
+ 'ubuntu:22.04',
+ args=('grep', '--color', 'root', '/etc/group'),
+ color=False,
+ )
+ assert ret == (0, b'root:x:0:\n')
diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py
index 5f7c85e7..e269976f 100644
--- a/tests/languages/docker_test.py
+++ b/tests/languages/docker_test.py
@@ -11,41 +11,176 @@ 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
-11:blkio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
-10:freezer:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
-9:cpu,cpuacct:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
-8:pids:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
-7:rdma:/
-6:net_cls,net_prio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
-5:cpuset:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
-4:devices:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
-3:memory:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
-2:perf_event:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
-1:name=systemd:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
-0::/system.slice/containerd.service
+DOCKER_CGROUPS_V1_MOUNTINFO_EXAMPLE = b'''\
+759 717 0:52 / / rw,relatime master:300 - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/PCPE5P5IVGM7CFCPJR353N3ONK:/var/lib/docker/overlay2/l/EQFSDHFAJ333VEMEJD4ZTRIZCB,upperdir=/var/lib/docker/overlay2/0d9f6bf186030d796505b87d6daa92297355e47641e283d3c09d83a7f221e462/diff,workdir=/var/lib/docker/overlay2/0d9f6bf186030d796505b87d6daa92297355e47641e283d3c09d83a7f221e462/work
+760 759 0:58 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw
+761 759 0:59 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64
+762 761 0:60 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666
+763 759 0:61 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro
+764 763 0:62 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755,inode64
+765 764 0:29 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/systemd ro,nosuid,nodev,noexec,relatime master:11 - cgroup cgroup rw,xattr,name=systemd
+766 764 0:32 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/rdma ro,nosuid,nodev,noexec,relatime master:15 - cgroup cgroup rw,rdma
+767 764 0:33 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/cpu,cpuacct ro,nosuid,nodev,noexec,relatime master:16 - cgroup cgroup rw,cpu,cpuacct
+768 764 0:34 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/cpuset ro,nosuid,nodev,noexec,relatime master:17 - cgroup cgroup rw,cpuset
+769 764 0:35 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/pids ro,nosuid,nodev,noexec,relatime master:18 - cgroup cgroup rw,pids
+770 764 0:36 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/memory ro,nosuid,nodev,noexec,relatime master:19 - cgroup cgroup rw,memory
+771 764 0:37 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/perf_event ro,nosuid,nodev,noexec,relatime master:20 - cgroup cgroup rw,perf_event
+772 764 0:38 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/net_cls,net_prio ro,nosuid,nodev,noexec,relatime master:21 - cgroup cgroup rw,net_cls,net_prio
+773 764 0:39 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/blkio ro,nosuid,nodev,noexec,relatime master:22 - cgroup cgroup rw,blkio
+774 764 0:40 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/misc ro,nosuid,nodev,noexec,relatime master:23 - cgroup cgroup rw,misc
+775 764 0:41 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/hugetlb ro,nosuid,nodev,noexec,relatime master:24 - cgroup cgroup rw,hugetlb
+776 764 0:42 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/devices ro,nosuid,nodev,noexec,relatime master:25 - cgroup cgroup rw,devices
+777 764 0:43 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/freezer ro,nosuid,nodev,noexec,relatime master:26 - cgroup cgroup rw,freezer
+778 761 0:57 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw
+779 761 0:63 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k,inode64
+780 759 8:5 /var/lib/docker/containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/sda5 rw,errors=remount-ro
+781 759 8:5 /var/lib/docker/containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/hostname /etc/hostname rw,relatime - ext4 /dev/sda5 rw,errors=remount-ro
+782 759 8:5 /var/lib/docker/containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/hosts /etc/hosts rw,relatime - ext4 /dev/sda5 rw,errors=remount-ro
+718 761 0:60 /0 /dev/console rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666
+719 760 0:58 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw
+720 760 0:58 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw
+721 760 0:58 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw
+722 760 0:58 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw
+723 760 0:58 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw
+724 760 0:64 / /proc/asound ro,relatime - tmpfs tmpfs ro,inode64
+725 760 0:65 / /proc/acpi ro,relatime - tmpfs tmpfs ro,inode64
+726 760 0:59 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64
+727 760 0:59 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64
+728 760 0:59 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64
+729 760 0:66 / /proc/scsi ro,relatime - tmpfs tmpfs ro,inode64
+730 763 0:67 / /sys/firmware ro,relatime - tmpfs tmpfs ro,inode64
+731 763 0:68 / /sys/devices/virtual/powercap ro,relatime - tmpfs tmpfs ro,inode64
+''' # noqa: E501
+
+DOCKER_CGROUPS_V2_MOUNTINFO_EXAMPLE = b'''\
+721 386 0:45 / / rw,relatime master:218 - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/QHZ7OM7P4AQD3XLG274ZPWAJCV:/var/lib/docker/overlay2/l/5RFG6SZWVGOG2NKEYXJDQCQYX5,upperdir=/var/lib/docker/overlay2/e4ad859fc5d4791932b9b976052f01fb0063e01de3cef916e40ae2121f6a166e/diff,workdir=/var/lib/docker/overlay2/e4ad859fc5d4791932b9b976052f01fb0063e01de3cef916e40ae2121f6a166e/work,nouserxattr
+722 721 0:48 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw
+723 721 0:50 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64
+724 723 0:51 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666
+725 721 0:52 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro
+726 725 0:26 / /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - cgroup2 cgroup rw,nsdelegate,memory_recursiveprot
+727 723 0:47 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw
+728 723 0:53 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k,inode64
+729 721 8:3 /var/lib/docker/containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/sda3 rw,errors=remount-ro
+730 721 8:3 /var/lib/docker/containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/hostname /etc/hostname rw,relatime - ext4 /dev/sda3 rw,errors=remount-ro
+731 721 8:3 /var/lib/docker/containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/hosts /etc/hosts rw,relatime - ext4 /dev/sda3 rw,errors=remount-ro
+387 723 0:51 /0 /dev/console rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666
+388 722 0:48 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw
+389 722 0:48 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw
+525 722 0:48 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw
+526 722 0:48 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw
+571 722 0:48 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw
+572 722 0:57 / /proc/asound ro,relatime - tmpfs tmpfs ro,inode64
+575 722 0:58 / /proc/acpi ro,relatime - tmpfs tmpfs ro,inode64
+576 722 0:50 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64
+577 722 0:50 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64
+578 722 0:50 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64
+579 722 0:59 / /proc/scsi ro,relatime - tmpfs tmpfs ro,inode64
+580 725 0:60 / /sys/firmware ro,relatime - tmpfs tmpfs ro,inode64
+''' # noqa: E501
+
+PODMAN_CGROUPS_V1_MOUNTINFO_EXAMPLE = b'''\
+1200 915 0:57 / / rw,relatime - overlay overlay rw,lowerdir=/home/asottile/.local/share/containers/storage/overlay/l/ZWAU3VY3ZHABQJRBUAFPBX7R5D,upperdir=/home/asottile/.local/share/containers/storage/overlay/72504ef163fda63838930450553b7306412ccad139a007626732b3dc43af5200/diff,workdir=/home/asottile/.local/share/containers/storage/overlay/72504ef163fda63838930450553b7306412ccad139a007626732b3dc43af5200/work,volatile,userxattr
+1204 1200 0:62 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw
+1205 1200 0:63 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,uid=1000,gid=1000,inode64
+1206 1200 0:64 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs rw
+1207 1205 0:65 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=100004,mode=620,ptmxmode=666
+1208 1205 0:61 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw
+1209 1200 0:53 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/.containerenv /run/.containerenv rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=814036k,mode=700,uid=1000,gid=1000,inode64
+1210 1200 0:53 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/resolv.conf /etc/resolv.conf rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=814036k,mode=700,uid=1000,gid=1000,inode64
+1211 1200 0:53 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/hosts /etc/hosts rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=814036k,mode=700,uid=1000,gid=1000,inode64
+1212 1205 0:56 / /dev/shm rw,relatime - tmpfs shm rw,size=64000k,uid=1000,gid=1000,inode64
+1213 1200 0:53 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/hostname /etc/hostname rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=814036k,mode=700,uid=1000,gid=1000,inode64
+1214 1206 0:66 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs cgroup rw,size=1024k,uid=1000,gid=1000,inode64
+1215 1214 0:43 / /sys/fs/cgroup/freezer ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,freezer
+1216 1214 0:42 /user.slice /sys/fs/cgroup/devices ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,devices
+1217 1214 0:41 / /sys/fs/cgroup/hugetlb ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,hugetlb
+1218 1214 0:40 / /sys/fs/cgroup/misc ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,misc
+1219 1214 0:39 / /sys/fs/cgroup/blkio ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,blkio
+1220 1214 0:38 / /sys/fs/cgroup/net_cls,net_prio ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,net_cls,net_prio
+1221 1214 0:37 / /sys/fs/cgroup/perf_event ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,perf_event
+1222 1214 0:36 /user.slice/user-1000.slice/user@1000.service /sys/fs/cgroup/memory ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,memory
+1223 1214 0:35 /user.slice/user-1000.slice/user@1000.service /sys/fs/cgroup/pids ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,pids
+1224 1214 0:34 / /sys/fs/cgroup/cpuset ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuset
+1225 1214 0:33 / /sys/fs/cgroup/cpu,cpuacct ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpu,cpuacct
+1226 1214 0:32 / /sys/fs/cgroup/rdma ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,rdma
+1227 1214 0:29 /user.slice/user-1000.slice/user@1000.service/apps.slice/apps-org.gnome.Terminal.slice/vte-spawn-0c50448e-b395-4d76-8b92-379f16e5066f.scope /sys/fs/cgroup/systemd ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,xattr,name=systemd
+1228 1205 0:5 /null /dev/null rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64
+1229 1205 0:5 /zero /dev/zero rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64
+1230 1205 0:5 /full /dev/full rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64
+1231 1205 0:5 /tty /dev/tty rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64
+1232 1205 0:5 /random /dev/random rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64
+1233 1205 0:5 /urandom /dev/urandom rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64
+1234 1204 0:67 / /proc/acpi ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64
+1235 1204 0:5 /null /proc/kcore rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64
+1236 1204 0:5 /null /proc/keys rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64
+1237 1204 0:5 /null /proc/timer_list rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64
+1238 1204 0:68 / /proc/scsi ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64
+1239 1206 0:69 / /sys/firmware ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64
+1240 1206 0:70 / /sys/dev/block ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64
+1241 1204 0:62 /asound /proc/asound ro,relatime - proc proc rw
+1242 1204 0:62 /bus /proc/bus ro,relatime - proc proc rw
+1243 1204 0:62 /fs /proc/fs ro,relatime - proc proc rw
+1244 1204 0:62 /irq /proc/irq ro,relatime - proc proc rw
+1245 1204 0:62 /sys /proc/sys ro,relatime - proc proc rw
+1256 1204 0:62 /sysrq-trigger /proc/sysrq-trigger ro,relatime - proc proc rw
+916 1205 0:65 /0 /dev/console rw,relatime - devpts devpts rw,gid=100004,mode=620,ptmxmode=666
+''' # noqa: E501
+
+PODMAN_CGROUPS_V2_MOUNTINFO_EXAMPLE = b'''\
+685 690 0:63 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/resolv.conf /etc/resolv.conf rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=1637624k,nr_inodes=409406,mode=700,uid=1000,gid=1000,inode64
+686 690 0:63 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/hosts /etc/hosts rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=1637624k,nr_inodes=409406,mode=700,uid=1000,gid=1000,inode64
+687 692 0:50 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=64000k,uid=1000,gid=1000,inode64
+688 690 0:63 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/.containerenv /run/.containerenv rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=1637624k,nr_inodes=409406,mode=700,uid=1000,gid=1000,inode64
+689 690 0:63 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/hostname /etc/hostname rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=1637624k,nr_inodes=409406,mode=700,uid=1000,gid=1000,inode64
+690 546 0:55 / / rw,relatime - overlay overlay rw,lowerdir=/home/asottile/.local/share/containers/storage/overlay/l/NPOHYOD3PI3YW6TQSGBOVOUSK6,upperdir=/home/asottile/.local/share/containers/storage/overlay/565c206fb79f876ffd5f069b8bd7a97fb5e47d5d07396b0c395a4ed6725d4a8e/diff,workdir=/home/asottile/.local/share/containers/storage/overlay/565c206fb79f876ffd5f069b8bd7a97fb5e47d5d07396b0c395a4ed6725d4a8e/work,redirect_dir=nofollow,uuid=on,volatile,userxattr
+691 690 0:59 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw
+692 690 0:61 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,uid=1000,gid=1000,inode64
+693 690 0:62 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs rw
+694 692 0:66 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=100004,mode=620,ptmxmode=666
+695 692 0:58 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw
+696 693 0:28 / /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - cgroup2 cgroup2 rw,nsdelegate,memory_recursiveprot
+698 692 0:6 /null /dev/null rw,nosuid,noexec,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64
+699 692 0:6 /zero /dev/zero rw,nosuid,noexec,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64
+700 692 0:6 /full /dev/full rw,nosuid,noexec,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64
+701 692 0:6 /tty /dev/tty rw,nosuid,noexec,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64
+702 692 0:6 /random /dev/random rw,nosuid,noexec,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64
+703 692 0:6 /urandom /dev/urandom rw,nosuid,noexec,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64
+704 691 0:67 / /proc/acpi ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64
+705 691 0:6 /null /proc/kcore ro,nosuid,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64
+706 691 0:6 /null /proc/keys ro,nosuid,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64
+707 691 0:6 /null /proc/latency_stats ro,nosuid,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64
+708 691 0:6 /null /proc/timer_list ro,nosuid,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64
+709 691 0:68 / /proc/scsi ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64
+710 693 0:69 / /sys/firmware ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64
+711 693 0:70 / /sys/dev/block ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64
+712 693 0:71 / /sys/devices/virtual/powercap ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64
+713 691 0:59 /asound /proc/asound ro,nosuid,nodev,noexec,relatime - proc proc rw
+714 691 0:59 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw
+715 691 0:59 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw
+716 691 0:59 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw
+717 691 0:59 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw
+718 691 0:59 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw
+547 692 0:66 /0 /dev/console rw,relatime - devpts devpts rw,gid=100004,mode=620,ptmxmode=666
''' # noqa: E501
# The ID should match the above cgroup example.
CONTAINER_ID = 'c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7' # noqa: E501
-NON_DOCKER_CGROUP_EXAMPLE = b'''\
-12:perf_event:/
-11:hugetlb:/
-10:devices:/
-9:blkio:/
-8:rdma:/
-7:cpuset:/
-6:cpu,cpuacct:/
-5:freezer:/
-4:memory:/
-3:pids:/
-2:net_cls,net_prio:/
-1:name=systemd:/init.scope
-0::/init.scope
-'''
+NON_DOCKER_MOUNTINFO_EXAMPLE = b'''\
+21 27 0:19 / /sys rw,nosuid,nodev,noexec,relatime shared:7 - sysfs sysfs rw
+22 27 0:20 / /proc rw,nosuid,nodev,noexec,relatime shared:14 - proc proc rw
+23 27 0:5 / /dev rw,nosuid,relatime shared:2 - devtmpfs udev rw,size=10219484k,nr_inodes=2554871,mode=755,inode64
+24 23 0:21 / /dev/pts rw,nosuid,noexec,relatime shared:3 - devpts devpts rw,gid=5,mode=620,ptmxmode=000
+25 27 0:22 / /run rw,nosuid,nodev,noexec,relatime shared:5 - tmpfs tmpfs rw,size=2047768k,mode=755,inode64
+27 1 8:2 / / rw,relatime shared:1 - ext4 /dev/sda2 rw,errors=remount-ro
+28 21 0:6 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime shared:8 - securityfs securityfs rw
+29 23 0:24 / /dev/shm rw,nosuid,nodev shared:4 - tmpfs tmpfs rw,inode64
+30 25 0:25 / /run/lock rw,nosuid,nodev,noexec,relatime shared:6 - tmpfs tmpfs rw,size=5120k,inode64
+''' # noqa: E501
def test_docker_fallback_user():
@@ -60,9 +195,46 @@ def test_docker_fallback_user():
assert docker.get_docker_user() == ()
-def test_in_docker_no_file():
+@pytest.fixture(autouse=True)
+def _avoid_cache():
+ with mock.patch.object(
+ docker,
+ '_is_rootless',
+ docker._is_rootless.__wrapped__,
+ ):
+ yield
+
+
+@pytest.mark.parametrize(
+ 'info_ret',
+ (
+ (0, b'{"SecurityOptions": ["name=rootless","name=cgroupns"]}', b''),
+ (0, b'{"host": {"security": {"rootless": true}}}', b''),
+ ),
+)
+def test_docker_user_rootless(info_ret):
+ with mock.patch.object(docker, 'cmd_output_b', return_value=info_ret):
+ assert docker.get_docker_user() == ()
+
+
+@pytest.mark.parametrize(
+ 'info_ret',
+ (
+ (0, b'{"SecurityOptions": ["name=cgroupns"]}', b''),
+ (0, b'{"host": {"security": {"rootless": false}}}', b''),
+ (0, b'{"response_from_some_other_container_engine": true}', b''),
+ (0, b'{"SecurityOptions": null}', b''),
+ (1, b'', b''),
+ ),
+)
+def test_docker_user_non_rootless(info_ret):
+ with mock.patch.object(docker, 'cmd_output_b', return_value=info_ret):
+ assert docker.get_docker_user() != ()
+
+
+def test_container_id_no_file():
with mock.patch.object(builtins, 'open', side_effect=FileNotFoundError):
- assert docker._is_in_docker() is False
+ assert docker._get_container_id() is None
def _mock_open(data):
@@ -74,38 +246,33 @@ def _mock_open(data):
)
-def test_in_docker_docker_in_file():
- with _mock_open(DOCKER_CGROUP_EXAMPLE):
- assert docker._is_in_docker() is True
-
-
-def test_in_docker_docker_not_in_file():
- with _mock_open(NON_DOCKER_CGROUP_EXAMPLE):
- assert docker._is_in_docker() is False
+def test_container_id_not_in_file():
+ with _mock_open(NON_DOCKER_MOUNTINFO_EXAMPLE):
+ assert docker._get_container_id() is None
def test_get_container_id():
- with _mock_open(DOCKER_CGROUP_EXAMPLE):
+ with _mock_open(DOCKER_CGROUPS_V1_MOUNTINFO_EXAMPLE):
+ assert docker._get_container_id() == CONTAINER_ID
+ with _mock_open(DOCKER_CGROUPS_V2_MOUNTINFO_EXAMPLE):
+ assert docker._get_container_id() == CONTAINER_ID
+ with _mock_open(PODMAN_CGROUPS_V1_MOUNTINFO_EXAMPLE):
+ assert docker._get_container_id() == CONTAINER_ID
+ with _mock_open(PODMAN_CGROUPS_V2_MOUNTINFO_EXAMPLE):
assert docker._get_container_id() == CONTAINER_ID
-def test_get_container_id_failure():
- with _mock_open(b''), pytest.raises(RuntimeError):
- docker._get_container_id()
-
-
def test_get_docker_path_not_in_docker_returns_same():
- with mock.patch.object(docker, '_is_in_docker', return_value=False):
+ with _mock_open(b''):
assert docker._get_docker_path('abc') == 'abc'
@pytest.fixture
def in_docker():
- with mock.patch.object(docker, '_is_in_docker', return_value=True):
- with mock.patch.object(
- docker, '_get_container_id', return_value=CONTAINER_ID,
- ):
- yield
+ with mock.patch.object(
+ docker, '_get_container_id', return_value=CONTAINER_ID,
+ ):
+ yield
def _linux_commonpath():
@@ -181,3 +348,26 @@ 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')
+
+
+@xfailif_windows # pragma: win32 no cover
+def test_docker_hook_mount_permissions(tmp_path):
+ dockerfile = '''\
+FROM ubuntu:22.04
+'''
+ tmp_path.joinpath('Dockerfile').write_text(dockerfile)
+
+ retcode, _ = run_language(tmp_path, docker, 'touch', ('README.md',))
+ assert retcode == 0
diff --git a/tests/languages/dotnet_test.py b/tests/languages/dotnet_test.py
index e69de29b..ee408256 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
+ net8
+ 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 00000000..7c74886f
--- /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 0219261f..7fb6ab18 100644
--- a/tests/languages/golang_test.py
+++ b/tests/languages/golang_test.py
@@ -1,13 +1,24 @@
from __future__ import annotations
-import re
from unittest import mock
import pytest
+import re_assert
import pre_commit.constants as C
+from pre_commit import lang_base
+from pre_commit.commands.install_uninstall import install
+from pre_commit.envcontext import envcontext
from pre_commit.languages import golang
-from pre_commit.languages import helpers
+from pre_commit.store import _make_local_repo
+from pre_commit.util import CalledProcessError
+from pre_commit.util import cmd_output
+from testing.fixtures import add_config_to_repo
+from testing.fixtures import make_config_from_repo
+from testing.language_helpers import run_language
+from testing.util import cmd_output_mocked_pre_commit_home
+from testing.util import cwd
+from testing.util import git_commit
ACTUAL_GET_DEFAULT_VERSION = golang.get_default_version.__wrapped__
@@ -15,7 +26,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
@@ -40,4 +51,186 @@ def test_golang_infer_go_version_default():
version = ACTUAL_INFER_GO_VERSION(C.DEFAULT)
assert version != C.DEFAULT
- assert re.match(r'^\d+\.\d+\.\d+$', version)
+ re_assert.Matches(r'^\d+\.\d+(?:\.\d+)?$').assert_matches(version)
+
+
+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.21.1',
+ )
+
+ assert ret == 0
+ assert out.startswith(b'go version go1.21.1')
+
+
+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, world!\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)
+
+
+def test_during_commit_all(tmp_path, tempdir_factory, store, in_git_dir):
+ hook_dir = tmp_path.joinpath('hook')
+ hook_dir.mkdir()
+ _make_hello_world(hook_dir)
+ hook_dir.joinpath('.pre-commit-hooks.yaml').write_text(
+ '- id: hello-world\n'
+ ' name: hello world\n'
+ ' entry: golang-hello-world\n'
+ ' language: golang\n'
+ ' always_run: true\n',
+ )
+ cmd_output('git', 'init', hook_dir)
+ cmd_output('git', 'add', '.', cwd=hook_dir)
+ git_commit(cwd=hook_dir)
+
+ add_config_to_repo(in_git_dir, make_config_from_repo(hook_dir))
+
+ assert not install(C.CONFIG_FILE, store, hook_types=['pre-commit'])
+
+ git_commit(
+ fn=cmd_output_mocked_pre_commit_home,
+ tempdir_factory=tempdir_factory,
+ )
+
+
+def test_automatic_toolchain_switching(tmp_path):
+ go_mod = '''\
+module toolchain-version-test
+
+go 1.23.1
+'''
+ main_go = '''\
+package main
+
+func main() {}
+'''
+ tmp_path.joinpath('go.mod').write_text(go_mod)
+ mod_dir = tmp_path.joinpath('toolchain-version-test')
+ mod_dir.mkdir()
+ main_file = mod_dir.joinpath('main.go')
+ main_file.write_text(main_go)
+
+ with pytest.raises(CalledProcessError) as excinfo:
+ run_language(
+ path=tmp_path,
+ language=golang,
+ version='1.22.0',
+ exe='golang-version-test',
+ )
+
+ assert 'go.mod requires go >= 1.23.1' in excinfo.value.stderr.decode()
+
+
+def test_automatic_toolchain_switching_go_fmt(tmp_path, monkeypatch):
+ go_mod_hook = '''\
+module toolchain-version-test
+
+go 1.22.0
+'''
+ go_mod = '''\
+module toolchain-version-test
+
+go 1.23.1
+'''
+ main_go = '''\
+package main
+
+func main() {}
+'''
+ hook_dir = tmp_path.joinpath('hook')
+ hook_dir.mkdir()
+ hook_dir.joinpath('go.mod').write_text(go_mod_hook)
+
+ test_dir = tmp_path.joinpath('test')
+ test_dir.mkdir()
+ test_dir.joinpath('go.mod').write_text(go_mod)
+ main_file = test_dir.joinpath('main.go')
+ main_file.write_text(main_go)
+
+ with cwd(test_dir):
+ ret, out = run_language(
+ path=hook_dir,
+ language=golang,
+ version='1.22.0',
+ exe='go fmt',
+ file_args=(str(main_file),),
+ )
+
+ assert ret == 1
+ assert 'go.mod requires go >= 1.23.1' in out.decode()
diff --git a/tests/languages/haskell_test.py b/tests/languages/haskell_test.py
new file mode 100644
index 00000000..f888109b
--- /dev/null
+++ b/tests/languages/haskell_test.py
@@ -0,0 +1,50 @@
+from __future__ import annotations
+
+import pytest
+
+from pre_commit.errors import FatalError
+from pre_commit.languages import haskell
+from pre_commit.util import win_exe
+from testing.language_helpers import run_language
+
+
+def test_run_example_executable(tmp_path):
+ example_cabal = '''\
+cabal-version: 2.4
+name: example
+version: 0.1.0.0
+
+executable example
+ main-is: Main.hs
+
+ build-depends: base >=4
+ default-language: Haskell2010
+'''
+ main_hs = '''\
+module Main where
+
+main :: IO ()
+main = putStrLn "Hello, Haskell!"
+'''
+ tmp_path.joinpath('example.cabal').write_text(example_cabal)
+ tmp_path.joinpath('Main.hs').write_text(main_hs)
+
+ result = run_language(tmp_path, haskell, 'example')
+ assert result == (0, b'Hello, Haskell!\n')
+
+ # should not symlink things into environments
+ exe = tmp_path.joinpath(win_exe('hs_env-default/bin/example'))
+ assert exe.is_file()
+ assert not exe.is_symlink()
+
+
+def test_run_dep(tmp_path):
+ result = run_language(tmp_path, haskell, 'hello', deps=['hello'])
+ assert result == (0, b'Hello, World!\n')
+
+
+def test_run_empty(tmp_path):
+ with pytest.raises(FatalError) as excinfo:
+ run_language(tmp_path, haskell, 'example')
+ msg, = excinfo.value.args
+ assert msg == 'Expected .cabal files or additional_dependencies'
diff --git a/tests/languages/julia_test.py b/tests/languages/julia_test.py
new file mode 100644
index 00000000..175622d6
--- /dev/null
+++ b/tests/languages/julia_test.py
@@ -0,0 +1,111 @@
+from __future__ import annotations
+
+import os
+from unittest import mock
+
+from pre_commit.languages import julia
+from testing.language_helpers import run_language
+from testing.util import cwd
+
+
+def _make_hook(tmp_path, julia_code):
+ src_dir = tmp_path.joinpath('src')
+ src_dir.mkdir()
+ src_dir.joinpath('main.jl').write_text(julia_code)
+ tmp_path.joinpath('Project.toml').write_text(
+ '[deps]\n'
+ 'Example = "7876af07-990d-54b4-ab0e-23690620f79a"\n',
+ )
+
+
+def test_julia_hook(tmp_path):
+ code = """
+ using Example
+ function main()
+ println("Hello, world!")
+ end
+ main()
+ """
+ _make_hook(tmp_path, code)
+ expected = (0, b'Hello, world!\n')
+ assert run_language(tmp_path, julia, 'src/main.jl') == expected
+
+
+def test_julia_hook_with_startup(tmp_path):
+ depot_path = tmp_path.joinpath('depot')
+ depot_path.joinpath('config').mkdir(parents=True)
+ startup = depot_path.joinpath('config', 'startup.jl')
+ startup.write_text('error("Startup file used!")\n')
+
+ depo_path_var = f'{depot_path}{os.pathsep}'
+ with mock.patch.dict(os.environ, {'JULIA_DEPOT_PATH': depo_path_var}):
+ test_julia_hook(tmp_path)
+
+
+def test_julia_hook_manifest(tmp_path):
+ code = """
+ using Example
+ println(pkgversion(Example))
+ """
+ _make_hook(tmp_path, code)
+
+ tmp_path.joinpath('Manifest.toml').write_text(
+ 'manifest_format = "2.0"\n\n'
+ '[[deps.Example]]\n'
+ 'git-tree-sha1 = "11820aa9c229fd3833d4bd69e5e75ef4e7273bf1"\n'
+ 'uuid = "7876af07-990d-54b4-ab0e-23690620f79a"\n'
+ 'version = "0.5.4"\n',
+ )
+ expected = (0, b'0.5.4\n')
+ assert run_language(tmp_path, julia, 'src/main.jl') == expected
+
+
+def test_julia_hook_args(tmp_path):
+ code = """
+ function main(argv)
+ foreach(println, argv)
+ end
+ main(ARGS)
+ """
+ _make_hook(tmp_path, code)
+ expected = (0, b'--arg1\n--arg2\n')
+ assert run_language(
+ tmp_path, julia, 'src/main.jl --arg1 --arg2',
+ ) == expected
+
+
+def test_julia_hook_additional_deps(tmp_path):
+ code = """
+ using TOML
+ function main()
+ project_file = Base.active_project()
+ dict = TOML.parsefile(project_file)
+ for (k, v) in dict["deps"]
+ println(k, " = ", v)
+ end
+ end
+ main()
+ """
+ _make_hook(tmp_path, code)
+ deps = ('TOML=fa267f1f-6049-4f14-aa54-33bafae1ed76',)
+ ret, out = run_language(tmp_path, julia, 'src/main.jl', deps=deps)
+ assert ret == 0
+ assert b'Example = 7876af07-990d-54b4-ab0e-23690620f79a' in out
+ assert b'TOML = fa267f1f-6049-4f14-aa54-33bafae1ed76' in out
+
+
+def test_julia_repo_local(tmp_path):
+ env_dir = tmp_path.joinpath('envdir')
+ env_dir.mkdir()
+ local_dir = tmp_path.joinpath('local')
+ local_dir.mkdir()
+ local_dir.joinpath('local.jl').write_text(
+ 'using TOML; foreach(println, ARGS)',
+ )
+ with cwd(local_dir):
+ deps = ('TOML=fa267f1f-6049-4f14-aa54-33bafae1ed76',)
+ expected = (0, b'--local-arg1\n--local-arg2\n')
+ assert run_language(
+ env_dir, julia, 'local.jl --local-arg1 --local-arg2',
+ deps=deps, is_local=True,
+ ) == expected
diff --git a/tests/languages/node_test.py b/tests/languages/node_test.py
index b69adfa6..055cb1e9 100644
--- a/tests/languages/node_test.py
+++ b/tests/languages/node_test.py
@@ -13,7 +13,9 @@ from pre_commit import envcontext
from pre_commit import parse_shebang
from pre_commit.languages import node
from pre_commit.prefix import Prefix
+from pre_commit.store import _make_local_repo
from pre_commit.util import cmd_output
+from testing.language_helpers import run_language
from testing.util import xfailif_windows
@@ -109,3 +111,42 @@ def test_installs_without_links_outside_env(tmpdir):
with node.in_env(prefix, 'system'):
assert cmd_output('foo')[1] == 'success!\n'
+
+
+def _make_hello_world(tmp_path):
+ package_json = '''\
+{"name": "t", "version": "0.0.1", "bin": {"node-hello": "./bin/main.js"}}
+'''
+ tmp_path.joinpath('package.json').write_text(package_json)
+ bin_dir = tmp_path.joinpath('bin')
+ bin_dir.mkdir()
+ bin_dir.joinpath('main.js').write_text(
+ '#!/usr/bin/env node\n'
+ 'console.log("Hello World");\n',
+ )
+
+
+def test_node_hook_system(tmp_path):
+ _make_hello_world(tmp_path)
+ ret = run_language(tmp_path, node, 'node-hello')
+ assert ret == (0, b'Hello World\n')
+
+
+def test_node_with_user_config_set(tmp_path):
+ cfg = tmp_path.joinpath('cfg')
+ cfg.write_text('cache=/dne\n')
+ with envcontext.envcontext((('NPM_CONFIG_USERCONFIG', str(cfg)),)):
+ test_node_hook_system(tmp_path)
+
+
+@pytest.mark.parametrize('version', (C.DEFAULT, '18.14.0'))
+def test_node_hook_versions(tmp_path, version):
+ _make_hello_world(tmp_path)
+ ret = run_language(tmp_path, node, 'node-hello', version=version)
+ assert ret == (0, b'Hello World\n')
+
+
+def test_node_additional_deps(tmp_path):
+ _make_local_repo(str(tmp_path))
+ ret, out = run_language(tmp_path, node, 'npm ls -g', deps=('lodash',))
+ assert b' lodash@' in out
diff --git a/tests/languages/pygrep_test.py b/tests/languages/pygrep_test.py
index 8420046c..c6271c80 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 54fb98fe..593634b7 100644
--- a/tests/languages/python_test.py
+++ b/tests/languages/python_test.py
@@ -10,8 +10,12 @@ import pre_commit.constants as C
from pre_commit.envcontext import envcontext
from pre_commit.languages import python
from pre_commit.prefix import Prefix
+from pre_commit.store import _make_local_repo
+from pre_commit.util import cmd_output_b
from pre_commit.util import make_executable
from pre_commit.util import win_exe
+from testing.auto_namedtuple import auto_namedtuple
+from testing.language_helpers import run_language
def test_read_pyvenv_cfg(tmpdir):
@@ -33,12 +37,78 @@ def test_read_pyvenv_cfg_non_utf8(tmpdir):
assert python._read_pyvenv_cfg(pyvenv_cfg) == expected
+def _get_default_version(
+ *,
+ impl: str,
+ exe: str,
+ found: set[str],
+ version: tuple[int, int],
+) -> str:
+ sys_exe = f'/fake/path/{exe}'
+ sys_impl = auto_namedtuple(name=impl)
+ sys_ver = auto_namedtuple(major=version[0], minor=version[1])
+
+ def find_exe(s):
+ if s in found:
+ return f'/fake/path/found/{exe}'
+ else:
+ return None
+
+ with (
+ mock.patch.object(sys, 'implementation', sys_impl),
+ mock.patch.object(sys, 'executable', sys_exe),
+ mock.patch.object(sys, 'version_info', sys_ver),
+ mock.patch.object(python, 'find_executable', find_exe),
+ ):
+ return python.get_default_version.__wrapped__()
+
+
+def test_default_version_sys_executable_found():
+ ret = _get_default_version(
+ impl='cpython',
+ exe='python3.12',
+ found={'python3.12'},
+ version=(3, 12),
+ )
+ assert ret == 'python3.12'
+
+
+def test_default_version_picks_specific_when_found():
+ ret = _get_default_version(
+ impl='cpython',
+ exe='python3',
+ found={'python3', 'python3.12'},
+ version=(3, 12),
+ )
+ assert ret == 'python3.12'
+
+
+def test_default_version_picks_pypy_versioned_exe():
+ ret = _get_default_version(
+ impl='pypy',
+ exe='python',
+ found={'pypy3.12', 'python3'},
+ version=(3, 12),
+ )
+ assert ret == 'pypy3.12'
+
+
+def test_default_version_picks_pypy_unversioned_exe():
+ ret = _get_default_version(
+ impl='pypy',
+ exe='python',
+ found={'pypy3', 'python3'},
+ version=(3, 12),
+ )
+ assert ret == 'pypy3'
+
+
def test_norm_version_expanduser():
home = os.path.expanduser('~')
- if os.name == 'nt': # pragma: nt cover
+ if sys.platform == 'win32': # pragma: win32 cover
path = r'~\python343'
expected_path = fr'{home}\python343'
- else: # pragma: nt no cover
+ else: # pragma: win32 no cover
path = '~/.pyenv/versions/3.4.3/bin/python'
expected_path = f'{home}/.pyenv/versions/3.4.3/bin/python'
result = python.norm_version(path)
@@ -210,3 +280,88 @@ 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')
+
+
+def _make_hello_hello(tmp_path):
+ setup_py = '''\
+from setuptools import setup
+
+setup(
+ name='socks',
+ version='0.0.0',
+ py_modules=['socks'],
+ entry_points={'console_scripts': ['socks = socks:main']},
+)
+'''
+
+ main_py = '''\
+import sys
+
+def main():
+ print(repr(sys.argv[1:]))
+ print('hello hello')
+ return 0
+'''
+ tmp_path.joinpath('setup.py').write_text(setup_py)
+ tmp_path.joinpath('socks.py').write_text(main_py)
+
+
+def test_simple_python_hook(tmp_path):
+ _make_hello_hello(tmp_path)
+
+ ret = run_language(tmp_path, python, 'socks', [os.devnull])
+ assert ret == (0, f'[{os.devnull!r}]\nhello hello\n'.encode())
+
+
+def test_simple_python_hook_default_version(tmp_path):
+ # make sure that this continues to work for platforms where default
+ # language detection does not work
+ with mock.patch.object(
+ python,
+ 'get_default_version',
+ return_value=C.DEFAULT,
+ ):
+ test_simple_python_hook(tmp_path)
+
+
+def test_python_hook_weird_setup_cfg(tmp_path):
+ _make_hello_hello(tmp_path)
+ setup_cfg = '[install]\ninstall_scripts=/usr/sbin'
+ tmp_path.joinpath('setup.cfg').write_text(setup_cfg)
+
+ ret = run_language(tmp_path, python, 'socks', [os.devnull])
+ assert ret == (0, f'[{os.devnull!r}]\nhello hello\n'.encode())
+
+
+def test_local_repo_with_other_artifacts(tmp_path):
+ cmd_output_b('git', 'init', tmp_path)
+ _make_local_repo(str(tmp_path))
+ # pretend a rust install also ran here
+ tmp_path.joinpath('target').mkdir()
+
+ ret, out = run_language(tmp_path, python, 'python --version')
+
+ assert ret == 0
+ assert out.startswith(b'Python ')
diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py
index 02c559cb..9e73129e 100644
--- a/tests/languages/r_test.py
+++ b/tests/languages/r_test.py
@@ -1,14 +1,17 @@
from __future__ import annotations
import os.path
-import shutil
+from unittest import mock
import pytest
+import pre_commit.constants as C
from pre_commit import envcontext
+from pre_commit import lang_base
from pre_commit.languages import r
from pre_commit.prefix import Prefix
from pre_commit.store import _make_local_repo
+from pre_commit.util import resource_text
from pre_commit.util import win_exe
from testing.language_helpers import run_language
@@ -127,7 +130,8 @@ def test_path_rscript_exec_no_r_home_set():
assert r._rscript_exec() == 'Rscript'
-def test_r_hook(tmp_path):
+@pytest.fixture
+def renv_lock_file(tmp_path):
renv_lock = '''\
{
"R": {
@@ -157,6 +161,12 @@ def test_r_hook(tmp_path):
}
}
'''
+ tmp_path.joinpath('renv.lock').write_text(renv_lock)
+ yield
+
+
+@pytest.fixture
+def description_file(tmp_path):
description = '''\
Package: gli.clu
Title: What the Package Does (One Line, Title Case)
@@ -178,27 +188,39 @@ RoxygenNote: 7.1.1
Imports:
rprojroot
'''
- hello_world_r = '''\
+ tmp_path.joinpath('DESCRIPTION').write_text(description)
+ yield
+
+
+@pytest.fixture
+def hello_world_file(tmp_path):
+ hello_world = '''\
stopifnot(
packageVersion('rprojroot') == '1.0',
packageVersion('gli.clu') == '0.0.0.9000'
)
cat("Hello, World, from R!\n")
'''
+ tmp_path.joinpath('hello-world.R').write_text(hello_world)
+ yield
- tmp_path.joinpath('renv.lock').write_text(renv_lock)
- tmp_path.joinpath('DESCRIPTION').write_text(description)
- tmp_path.joinpath('hello-world.R').write_text(hello_world_r)
+
+@pytest.fixture
+def renv_folder(tmp_path):
renv_dir = tmp_path.joinpath('renv')
renv_dir.mkdir()
- shutil.copy(
- os.path.join(
- os.path.dirname(__file__),
- '../../pre_commit/resources/empty_template_activate.R',
- ),
- renv_dir.joinpath('activate.R'),
- )
+ activate_r = resource_text('empty_template_activate.R')
+ renv_dir.joinpath('activate.R').write_text(activate_r)
+ yield
+
+def test_r_hook(
+ tmp_path,
+ renv_lock_file,
+ description_file,
+ hello_world_file,
+ renv_folder,
+):
expected = (0, b'Hello, World, from R!\n')
assert run_language(tmp_path, r, 'Rscript hello-world.R') == expected
@@ -221,3 +243,55 @@ Rscript -e '
args=('hi', 'hello'),
)
assert ret == (0, b'hi, hello, from R!\n')
+
+
+@pytest.fixture
+def prefix(tmpdir):
+ yield Prefix(str(tmpdir))
+
+
+@pytest.fixture
+def installed_environment(
+ renv_lock_file,
+ hello_world_file,
+ renv_folder,
+ prefix,
+):
+ env_dir = lang_base.environment_dir(
+ prefix, r.ENVIRONMENT_DIR, r.get_default_version(),
+ )
+ r.install_environment(prefix, C.DEFAULT, ())
+ yield prefix, env_dir
+
+
+def test_health_check_healthy(installed_environment):
+ # should be healthy right after creation
+ prefix, _ = installed_environment
+ assert r.health_check(prefix, C.DEFAULT) is None
+
+
+def test_health_check_after_downgrade(installed_environment):
+ prefix, _ = installed_environment
+
+ # pretend the saved installed version is old
+ with mock.patch.object(r, '_read_installed_version', return_value='1.0.0'):
+ output = r.health_check(prefix, C.DEFAULT)
+
+ assert output is not None
+ assert output.startswith('Hooks were installed for R version')
+
+
+@pytest.mark.parametrize('version', ('NULL', 'NA', "''"))
+def test_health_check_without_version(prefix, installed_environment, version):
+ prefix, env_dir = installed_environment
+
+ # simulate old pre-commit install by unsetting the installed version
+ r._execute_r_in_renv(
+ f'renv::settings$r.version({version})',
+ prefix=prefix, version=C.DEFAULT, cwd=env_dir,
+ )
+
+ # no R version specified fails as unhealty
+ msg = 'Hooks were installed with an unknown R version'
+ check_output = r.health_check(prefix, C.DEFAULT)
+ assert check_output is not None and check_output.startswith(msg)
diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py
index 63a16eb1..5d767b25 100644
--- a/tests/languages/ruby_test.py
+++ b/tests/languages/ruby_test.py
@@ -1,6 +1,5 @@
from __future__ import annotations
-import os.path
import tarfile
from unittest import mock
@@ -8,10 +7,12 @@ import pytest
import pre_commit.constants as C
from pre_commit import parse_shebang
+from pre_commit.envcontext import envcontext
from pre_commit.languages import ruby
-from pre_commit.prefix import Prefix
-from pre_commit.util import cmd_output
-from pre_commit.util import resource_bytesio
+from pre_commit.languages.ruby import _resource_bytesio
+from pre_commit.store import _make_local_repo
+from testing.language_helpers import run_language
+from testing.util import cwd
from testing.util import xfailif_windows
@@ -34,56 +35,105 @@ def test_uses_system_if_both_gem_and_ruby_are_available(find_exe_mck):
assert ACTUAL_GET_DEFAULT_VERSION() == 'system'
-@pytest.fixture
-def fake_gem_prefix(tmpdir):
- gemspec = '''\
-Gem::Specification.new do |s|
- s.name = 'pre_commit_placeholder_package'
- s.version = '0.0.0'
- s.summary = 'placeholder gem for pre-commit hooks'
- s.authors = ['Anthony Sottile']
-end
-'''
- tmpdir.join('placeholder_gem.gemspec').write(gemspec)
- yield Prefix(tmpdir)
-
-
-@xfailif_windows # pragma: win32 no cover
-def test_install_ruby_system(fake_gem_prefix):
- ruby.install_environment(fake_gem_prefix, 'system', ())
-
- # Should be able to activate and use rbenv install
- with ruby.in_env(fake_gem_prefix, 'system'):
- _, out, _ = cmd_output('gem', 'list')
- assert 'pre_commit_placeholder_package' in out
-
-
-@xfailif_windows # pragma: win32 no cover
-def test_install_ruby_default(fake_gem_prefix):
- ruby.install_environment(fake_gem_prefix, C.DEFAULT, ())
- # Should have created rbenv directory
- assert os.path.exists(fake_gem_prefix.path('rbenv-default'))
-
- # Should be able to activate using our script and access rbenv
- with ruby.in_env(fake_gem_prefix, 'default'):
- cmd_output('rbenv', '--help')
-
-
-@xfailif_windows # pragma: win32 no cover
-def test_install_ruby_with_version(fake_gem_prefix):
- ruby.install_environment(fake_gem_prefix, '3.2.0', ())
-
- # Should be able to activate and use rbenv install
- with ruby.in_env(fake_gem_prefix, '3.2.0'):
- cmd_output('rbenv', 'install', '--help')
-
-
@pytest.mark.parametrize(
'filename',
('rbenv.tar.gz', 'ruby-build.tar.gz', 'ruby-download.tar.gz'),
)
def test_archive_root_stat(filename):
- with resource_bytesio(filename) as f:
+ with _resource_bytesio(filename) as f:
with tarfile.open(fileobj=f) as tarf:
root, _, _ = filename.partition('.')
assert oct(tarf.getmember(root).mode) == '0o755'
+
+
+def _setup_hello_world(tmp_path):
+ bin_dir = tmp_path.joinpath('bin')
+ bin_dir.mkdir()
+ bin_dir.joinpath('ruby_hook').write_text(
+ '#!/usr/bin/env ruby\n'
+ "puts 'Hello world from a ruby hook'\n",
+ )
+ gemspec = '''\
+Gem::Specification.new do |s|
+ s.name = 'ruby_hook'
+ s.version = '0.1.0'
+ s.authors = ['Anthony Sottile']
+ s.summary = 'A ruby hook!'
+ s.description = 'A ruby hook!'
+ s.files = ['bin/ruby_hook']
+ s.executables = ['ruby_hook']
+end
+'''
+ tmp_path.joinpath('ruby_hook.gemspec').write_text(gemspec)
+
+
+def test_ruby_hook_system(tmp_path):
+ assert ruby.get_default_version() == 'system'
+
+ _setup_hello_world(tmp_path)
+
+ ret = run_language(tmp_path, ruby, 'ruby_hook')
+ assert ret == (0, b'Hello world from a ruby hook\n')
+
+
+def test_ruby_with_user_install_set(tmp_path):
+ gemrc = tmp_path.joinpath('gemrc')
+ gemrc.write_text('gem: --user-install\n')
+
+ with envcontext((('GEMRC', str(gemrc)),)):
+ test_ruby_hook_system(tmp_path)
+
+
+def test_ruby_additional_deps(tmp_path):
+ _make_local_repo(tmp_path)
+
+ ret = run_language(
+ tmp_path,
+ ruby,
+ 'ruby -e',
+ args=('require "jmespath"',),
+ deps=('jmespath',),
+ )
+ assert ret == (0, b'')
+
+
+@xfailif_windows # pragma: win32 no cover
+def test_ruby_hook_default(tmp_path):
+ _setup_hello_world(tmp_path)
+
+ out, ret = run_language(tmp_path, ruby, 'rbenv --help', version='default')
+ assert out == 0
+ assert ret.startswith(b'Usage: rbenv ')
+
+
+@xfailif_windows # pragma: win32 no cover
+def test_ruby_hook_language_version(tmp_path):
+ _setup_hello_world(tmp_path)
+ tmp_path.joinpath('bin', 'ruby_hook').write_text(
+ '#!/usr/bin/env ruby\n'
+ 'puts RUBY_VERSION\n'
+ "puts 'Hello world from a ruby hook'\n",
+ )
+
+ ret = run_language(tmp_path, ruby, 'ruby_hook', version='3.2.0')
+ assert ret == (0, b'3.2.0\nHello world from a ruby hook\n')
+
+
+@xfailif_windows # pragma: win32 no cover
+def test_ruby_with_bundle_disable_shared_gems(tmp_path):
+ workdir = tmp_path.joinpath('workdir')
+ workdir.mkdir()
+ # this needs a `source` or there's a deprecation warning
+ # silencing this with `BUNDLE_GEMFILE` breaks some tools (#2739)
+ workdir.joinpath('Gemfile').write_text('source ""\ngem "lol_hai"\n')
+ # this bundle config causes things to be written elsewhere
+ bundle = workdir.joinpath('.bundle')
+ bundle.mkdir()
+ bundle.joinpath('config').write_text(
+ 'BUNDLE_DISABLE_SHARED_GEMS: true\n'
+ 'BUNDLE_PATH: vendor/gem\n',
+ )
+
+ with cwd(workdir):
+ # `3.2.0` has new enough `gem` reading `.bundle`
+ test_ruby_hook_language_version(tmp_path)
diff --git a/tests/languages/rust_test.py b/tests/languages/rust_test.py
index b8167a9e..52e35613 100644
--- a/tests/languages/rust_test.py
+++ b/tests/languages/rust_test.py
@@ -1,6 +1,5 @@
from __future__ import annotations
-from typing import Mapping
from unittest import mock
import pytest
@@ -8,8 +7,9 @@ import pytest
import pre_commit.constants as C
from pre_commit import parse_shebang
from pre_commit.languages import rust
-from pre_commit.prefix import Prefix
-from pre_commit.util import cmd_output
+from pre_commit.store import _make_local_repo
+from testing.language_helpers import run_language
+from testing.util import cwd
ACTUAL_GET_DEFAULT_VERSION = rust.get_default_version.__wrapped__
@@ -30,64 +30,86 @@ def test_uses_default_when_rust_is_not_available(cmd_output_b_mck):
assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT
-@pytest.mark.parametrize('language_version', (C.DEFAULT, '1.56.0'))
-def test_installs_with_bootstrapped_rustup(tmpdir, language_version):
- tmpdir.join('src', 'main.rs').ensure().write(
+def test_selects_system_even_if_rust_toolchain_toml(tmp_path):
+ toolchain_toml = '[toolchain]\nchannel = "wtf"\n'
+ tmp_path.joinpath('rust-toolchain.toml').write_text(toolchain_toml)
+
+ with cwd(tmp_path):
+ assert ACTUAL_GET_DEFAULT_VERSION() == 'system'
+
+
+def _make_hello_world(tmp_path):
+ src_dir = tmp_path.joinpath('src')
+ src_dir.mkdir()
+ src_dir.joinpath('main.rs').write_text(
'fn main() {\n'
' println!("Hello, world!");\n'
'}\n',
)
- tmpdir.join('Cargo.toml').ensure().write(
+ tmp_path.joinpath('Cargo.toml').write_text(
'[package]\n'
'name = "hello_world"\n'
'version = "0.1.0"\n'
'edition = "2021"\n',
)
- prefix = Prefix(str(tmpdir))
- find_executable_exes = []
- original_find_executable = parse_shebang.find_executable
+def test_installs_rust_missing_rustup(tmp_path):
+ _make_hello_world(tmp_path)
- def mocked_find_executable(
- exe: str, *, env: Mapping[str, str] | None = None,
- ) -> str | None:
- """
- Return `None` the first time `find_executable` is called to ensure
- that the bootstrapping code is executed, then just let the function
- work as normal.
+ # pretend like `rustup` doesn't exist so it gets bootstrapped
+ calls = []
+ orig = parse_shebang.find_executable
- Also log the arguments to ensure that everything works as expected.
- """
- find_executable_exes.append(exe)
- if len(find_executable_exes) == 1:
+ def mck(exe, env=None):
+ calls.append(exe)
+ if len(calls) == 1:
+ assert exe == 'rustup'
return None
- return original_find_executable(exe, env=env)
+ return orig(exe, env=env)
- with mock.patch.object(parse_shebang, 'find_executable') as find_exe_mck:
- find_exe_mck.side_effect = mocked_find_executable
- rust.install_environment(prefix, language_version, ())
- assert find_executable_exes == ['rustup', 'rustup', 'cargo']
-
- with rust.in_env(prefix, language_version):
- assert cmd_output('hello_world')[1] == 'Hello, world!\n'
+ with mock.patch.object(parse_shebang, 'find_executable', side_effect=mck):
+ ret = run_language(tmp_path, rust, 'hello_world', version='1.56.0')
+ assert calls == ['rustup', 'rustup', 'cargo', 'hello_world']
+ assert ret == (0, b'Hello, world!\n')
-def test_installs_with_existing_rustup(tmpdir):
- tmpdir.join('src', 'main.rs').ensure().write(
- 'fn main() {\n'
- ' println!("Hello, world!");\n'
- '}\n',
- )
- tmpdir.join('Cargo.toml').ensure().write(
- '[package]\n'
- 'name = "hello_world"\n'
- 'version = "0.1.0"\n'
- 'edition = "2021"\n',
- )
- prefix = Prefix(str(tmpdir))
-
+@pytest.mark.parametrize('version', (C.DEFAULT, '1.56.0'))
+def test_language_version_with_rustup(tmp_path, version):
assert parse_shebang.find_executable('rustup') is not None
- rust.install_environment(prefix, '1.56.0', ())
- with rust.in_env(prefix, '1.56.0'):
- assert cmd_output('hello_world')[1] == 'Hello, world!\n'
+
+ _make_hello_world(tmp_path)
+
+ ret = run_language(tmp_path, rust, 'hello_world', version=version)
+ assert ret == (0, b'Hello, world!\n')
+
+
+@pytest.mark.parametrize('dep', ('cli:shellharden:4.2.0', 'cli:shellharden'))
+def test_rust_cli_additional_dependencies(tmp_path, dep):
+ _make_local_repo(str(tmp_path))
+
+ t_sh = tmp_path.joinpath('t.sh')
+ t_sh.write_text('echo $hi\n')
+
+ assert rust.get_default_version() == 'system'
+ ret = run_language(
+ tmp_path,
+ rust,
+ 'shellharden --transform',
+ deps=(dep,),
+ args=(str(t_sh),),
+ )
+ assert ret == (0, b'echo "$hi"\n')
+
+
+def test_run_lib_additional_dependencies(tmp_path):
+ _make_hello_world(tmp_path)
+
+ deps = ('shellharden:4.2.0', 'git-version')
+ ret = run_language(tmp_path, rust, 'hello_world', deps=deps)
+ assert ret == (0, b'Hello, world!\n')
+
+ bin_dir = tmp_path.joinpath('rustenv-system', 'bin')
+ assert bin_dir.is_dir()
+ assert not bin_dir.joinpath('shellharden').exists()
+ assert not bin_dir.joinpath('shellharden.exe').exists()
diff --git a/tests/languages/unsupported_script_test.py b/tests/languages/unsupported_script_test.py
new file mode 100644
index 00000000..b15b67e7
--- /dev/null
+++ b/tests/languages/unsupported_script_test.py
@@ -0,0 +1,14 @@
+from __future__ import annotations
+
+from pre_commit.languages import unsupported_script
+from pre_commit.util import make_executable
+from testing.language_helpers import run_language
+
+
+def test_unsupported_script_language(tmp_path):
+ exe = tmp_path.joinpath('main')
+ exe.write_text('#!/usr/bin/env bash\necho hello hello world\n')
+ make_executable(exe)
+
+ expected = (0, b'hello hello world\n')
+ assert run_language(tmp_path, unsupported_script, 'main') == expected
diff --git a/tests/languages/unsupported_test.py b/tests/languages/unsupported_test.py
new file mode 100644
index 00000000..7f8461e0
--- /dev/null
+++ b/tests/languages/unsupported_test.py
@@ -0,0 +1,10 @@
+from __future__ import annotations
+
+from pre_commit.languages import unsupported
+from testing.language_helpers import run_language
+
+
+def test_unsupported_language(tmp_path):
+ expected = (0, b'hello hello world\n')
+ ret = run_language(tmp_path, unsupported, 'echo hello hello world')
+ assert ret == expected
diff --git a/tests/main_test.py b/tests/main_test.py
index 51159262..fed085fc 100644
--- a/tests/main_test.py
+++ b/tests/main_test.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import argparse
+import contextlib
import os.path
from unittest import mock
@@ -8,6 +9,7 @@ import pytest
import pre_commit.constants as C
from pre_commit import main
+from pre_commit.commands import hazmat
from pre_commit.errors import FatalError
from pre_commit.util import cmd_output
from testing.auto_namedtuple import auto_namedtuple
@@ -97,11 +99,9 @@ CMDS = tuple(fn.replace('_', '-') for fn in FNS)
@pytest.fixture
def mock_commands():
- mcks = {fn: mock.patch.object(main, fn).start() for fn in FNS}
- ret = auto_namedtuple(**mcks)
- yield ret
- for mck in ret:
- mck.stop()
+ with contextlib.ExitStack() as ctx:
+ mcks = {f: ctx.enter_context(mock.patch.object(main, f)) for f in FNS}
+ yield auto_namedtuple(**mcks)
@pytest.fixture
@@ -158,6 +158,17 @@ def test_all_cmds(command, mock_commands, mock_store_dir):
assert_only_one_mock_called(mock_commands)
+def test_hazmat(mock_store_dir):
+ with mock.patch.object(hazmat, 'impl') as mck:
+ main.main(('hazmat', 'cd', 'subdir', '--', 'cmd', '--', 'f1', 'f2'))
+ assert mck.call_count == 1
+ (arg,), dct = mck.call_args
+ assert dct == {}
+ assert arg.tool == 'cd'
+ assert arg.subdir == 'subdir'
+ assert arg.cmd == ['cmd', '--', 'f1', 'f2']
+
+
def test_try_repo(mock_store_dir):
with mock.patch.object(main, 'try_repo') as patch:
main.main(('try-repo', '.'))
@@ -216,3 +227,9 @@ def test_expected_fatal_error_no_git_repo(in_tmpdir, cap_out, mock_store_dir):
'Is it installed, and are you in a Git repository directory?'
)
assert cap_out_lines[-1] == f'Check the log at {log_file}'
+
+
+def test_hook_stage_migration(mock_store_dir):
+ with mock.patch.object(main, 'run') as mck:
+ main.main(('run', '--hook-stage', 'commit'))
+ assert mck.call_args[0][2].hook_stage == 'pre-commit'
diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py
index 2fcb29ee..bd4384df 100644
--- a/tests/parse_shebang_test.py
+++ b/tests/parse_shebang_test.py
@@ -94,7 +94,7 @@ def test_normexe_does_not_exist_sep():
assert excinfo.value.args == ('Executable `./i-dont-exist-lol` not found',)
-@pytest.mark.xfail(os.name == 'nt', reason='posix only')
+@pytest.mark.xfail(sys.platform == 'win32', reason='posix only')
def test_normexe_not_executable(tmpdir): # pragma: win32 no cover
tmpdir.join('exe').ensure()
with tmpdir.as_cwd(), pytest.raises(OSError) as excinfo:
@@ -133,17 +133,17 @@ def test_normalize_cmd_PATH():
def test_normalize_cmd_shebang(in_tmpdir):
- echo = _echo_exe().replace(os.sep, '/')
- path = write_executable(echo)
- assert parse_shebang.normalize_cmd((path,)) == (echo, path)
+ us = sys.executable.replace(os.sep, '/')
+ path = write_executable(us)
+ assert parse_shebang.normalize_cmd((path,)) == (us, path)
def test_normalize_cmd_PATH_shebang_full_path(in_tmpdir):
- echo = _echo_exe().replace(os.sep, '/')
- path = write_executable(echo)
+ us = sys.executable.replace(os.sep, '/')
+ path = write_executable(us)
with bin_on_path():
ret = parse_shebang.normalize_cmd(('run',))
- assert ret == (echo, os.path.abspath(path))
+ assert ret == (us, os.path.abspath(path))
def test_normalize_cmd_PATH_shebang_PATH(in_tmpdir):
diff --git a/tests/repository_test.py b/tests/repository_test.py
index 85cf4581..5d71c3e4 100644
--- a/tests/repository_test.py
+++ b/tests/repository_test.py
@@ -1,27 +1,23 @@
from __future__ import annotations
import os.path
+import shlex
import shutil
+import sys
from typing import Any
from unittest import mock
import cfgv
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 node
from pre_commit.languages import python
-from pre_commit.languages import ruby
-from pre_commit.languages import rust
-from pre_commit.languages.all import languages
+from pre_commit.languages import unsupported
from pre_commit.prefix import Prefix
from pre_commit.repository import _hook_installed
from pre_commit.repository import all_hooks
@@ -30,28 +26,24 @@ from pre_commit.util import cmd_output
from pre_commit.util import cmd_output_b
from testing.fixtures import make_config_from_repo
from testing.fixtures import make_repo
-from testing.fixtures import modify_manifest
+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
-from testing.util import xfailif_windows
-
-
-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):
@@ -85,334 +77,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
-
-
-def test_python_hook(tempdir_factory, store):
- _test_hook_repo(
- tempdir_factory, store, 'python_hooks_repo',
- 'foo', [os.devnull],
- f'[{os.devnull!r}]\nHello World\n'.encode(),
- )
-
-
-def test_python_hook_default_version(tempdir_factory, store):
- # make sure that this continues to work for platforms where default
- # language detection does not work
- with mock.patch.object(
- python,
- 'get_default_version',
- return_value=C.DEFAULT,
- ):
- test_python_hook(tempdir_factory, store)
-
-
-def test_python_hook_args_with_spaces(tempdir_factory, store):
- _test_hook_repo(
- tempdir_factory, store, 'python_hooks_repo',
- 'foo',
- [],
- b"['i have spaces', 'and\"\\'quotes', '$and !this']\n"
- b'Hello World\n',
- config_kwargs={
- 'hooks': [{
- 'id': 'foo',
- 'args': ['i have spaces', 'and"\'quotes', '$and !this'],
- }],
- },
- )
-
-
-def test_python_hook_weird_setup_cfg(in_git_dir, tempdir_factory, store):
- in_git_dir.join('setup.cfg').write('[install]\ninstall_scripts=/usr/sbin')
-
- _test_hook_repo(
- tempdir_factory, store, 'python_hooks_repo',
- 'foo', [os.devnull],
- f'[{os.devnull!r}]\nHello World\n'.encode(),
- )
-
-
-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_run_a_node_hook(tempdir_factory, store):
- _test_hook_repo(
- tempdir_factory, store, 'node_hooks_repo',
- 'foo', [os.devnull], b'Hello World\n',
- )
-
-
-def test_run_a_node_hook_default_version(tempdir_factory, store):
- # make sure that this continues to work for platforms where node is not
- # installed at the system
- with mock.patch.object(
- node,
- 'get_default_version',
- return_value=C.DEFAULT,
- ):
- test_run_a_node_hook(tempdir_factory, store)
-
-
-def test_run_versioned_node_hook(tempdir_factory, store):
- _test_hook_repo(
- tempdir_factory, store, 'node_versioned_hooks_repo',
- 'versioned-node-hook', [os.devnull], b'v9.3.0\nHello World\n',
- )
-
-
-def test_node_hook_with_npm_userconfig_set(tempdir_factory, store, tmpdir):
- cfg = tmpdir.join('cfg')
- cfg.write('cache=/dne\n')
- with mock.patch.dict(os.environ, NPM_CONFIG_USERCONFIG=str(cfg)):
- test_run_a_node_hook(tempdir_factory, store)
-
-
-def test_run_a_ruby_hook(tempdir_factory, store):
- _test_hook_repo(
- tempdir_factory, store, 'ruby_hooks_repo',
- 'ruby_hook', [os.devnull], b'Hello world from a ruby hook\n',
- )
-
-
-def test_run_a_ruby_hook_with_user_install_set(tempdir_factory, store, tmpdir):
- gemrc = tmpdir.join('gemrc')
- gemrc.write('gem: --user-install\n')
- with envcontext((('GEMRC', str(gemrc)),)):
- test_run_a_ruby_hook(tempdir_factory, store)
-
-
-@xfailif_windows # pragma: win32 no cover
-def test_run_versioned_ruby_hook(tempdir_factory, store):
- _test_hook_repo(
- tempdir_factory, store, 'ruby_versioned_hooks_repo',
- 'ruby_hook',
- [os.devnull],
- b'3.2.0\nHello world from a ruby hook\n',
- )
-
-
-@xfailif_windows # pragma: win32 no cover
-def test_run_ruby_hook_with_disable_shared_gems(
- tempdir_factory,
- store,
- tmpdir,
-):
- """Make sure a Gemfile in the project doesn't interfere."""
- tmpdir.join('Gemfile').write('gem "lol_hai"')
- tmpdir.join('.bundle').mkdir()
- tmpdir.join('.bundle', 'config').write(
- 'BUNDLE_DISABLE_SHARED_GEMS: true\n'
- 'BUNDLE_PATH: vendor/gem\n',
- )
- with cwd(tmpdir.strpath):
- _test_hook_repo(
- tempdir_factory, store, 'ruby_versioned_hooks_repo',
- 'ruby_hook',
- [os.devnull],
- b'3.2.0\nHello world from a ruby hook\n',
- )
-
-
-def test_system_hook_with_spaces(tempdir_factory, store):
- _test_hook_repo(
- tempdir_factory, store, 'system_hook_with_spaces_repo',
- 'system-hook-with-spaces', [os.devnull], b'Hello World\n',
- )
-
-
-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_rust_hook(tempdir_factory, store):
- _test_hook_repo(
- tempdir_factory, store, 'rust_hooks_repo',
- 'rust-hook', [], b'hello world\n',
- )
-
-
-@pytest.mark.parametrize('dep', ('cli:shellharden:3.1.0', 'cli:shellharden'))
-def test_additional_rust_cli_dependencies_installed(
- tempdir_factory, store, dep,
-):
- path = make_repo(tempdir_factory, 'rust_hooks_repo')
- config = make_config_from_repo(path)
- # A small rust package with no dependencies.
- config['hooks'][0]['additional_dependencies'] = [dep]
- hook = _get_hook(config, store, 'rust-hook')
- envdir = helpers.environment_dir(
- hook.prefix,
- rust.ENVIRONMENT_DIR,
- 'system',
- )
- binaries = os.listdir(os.path.join(envdir, 'bin'))
- # normalize for windows
- binaries = [os.path.splitext(binary)[0] for binary in binaries]
- assert 'shellharden' in binaries
-
-
-def test_additional_rust_lib_dependencies_installed(
- tempdir_factory, store,
-):
- path = make_repo(tempdir_factory, 'rust_hooks_repo')
- config = make_config_from_repo(path)
- # A small rust package with no dependencies.
- deps = ['shellharden:3.1.0', 'git-version']
- config['hooks'][0]['additional_dependencies'] = deps
- hook = _get_hook(config, store, 'rust-hook')
- envdir = helpers.environment_dir(
- hook.prefix,
- rust.ENVIRONMENT_DIR,
- 'system',
- )
- binaries = os.listdir(os.path.join(envdir, 'bin'))
- # normalize for windows
- binaries = [os.path.splitext(binary)[0] for binary in binaries]
- assert 'rust-hello-world' in binaries
- assert 'shellharden' not in binaries
+ assert out == expected
def test_missing_executable(tempdir_factory, store):
@@ -464,7 +129,7 @@ def test_intermixed_stdout_stderr(tempdir_factory, store):
)
-@pytest.mark.xfail(os.name == 'nt', reason='ptys are posix-only')
+@pytest.mark.xfail(sys.platform == 'win32', reason='ptys are posix-only')
def test_output_isatty(tempdir_factory, store):
_test_hook_repo(
tempdir_factory, store, 'stdout_stderr_repo',
@@ -475,52 +140,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
@@ -570,7 +189,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,
@@ -579,105 +198,6 @@ def test_repository_state_compatibility(tempdir_factory, store, v):
assert _hook_installed(hook) is True
-def test_additional_ruby_dependencies_installed(tempdir_factory, store):
- path = make_repo(tempdir_factory, 'ruby_hooks_repo')
- config = make_config_from_repo(path)
- config['hooks'][0]['additional_dependencies'] = ['tins']
- hook = _get_hook(config, store, 'ruby_hook')
- with ruby.in_env(hook.prefix, hook.language_version):
- output = cmd_output('gem', 'list', '--local')[1]
- assert 'tins' in output
-
-
-def test_additional_node_dependencies_installed(tempdir_factory, store):
- path = make_repo(tempdir_factory, 'node_hooks_repo')
- config = make_config_from_repo(path)
- # Careful to choose a small package that's not depped by npm
- config['hooks'][0]['additional_dependencies'] = ['lodash']
- hook = _get_hook(config, store, 'foo')
- with node.in_env(hook.prefix, hook.language_version):
- output = cmd_output('npm', 'ls', '-g')[1]
- assert 'lodash' in output
-
-
-def test_additional_golang_dependencies_installed(
- tempdir_factory, store,
-):
- 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_local_rust_additional_dependencies(store):
- config = {
- 'repo': 'local',
- 'hooks': [{
- 'id': 'hello',
- 'name': 'hello',
- 'entry': 'hello',
- 'language': 'rust',
- 'additional_dependencies': ['cli:hello-cli:0.2.2'],
- }],
- }
- hook = _get_hook(config, store, 'hello')
- ret, out = _hook_run(hook, (), color=False)
- assert ret == 0
- assert _norm_out(out) == b'Hello World!\n'
-
-
-def test_fail_hooks(store):
- config = {
- 'repo': 'local',
- 'hooks': [{
- 'id': 'fail',
- 'name': 'fail',
- 'language': 'fail',
- 'entry': 'make sure to name changelogs as .rst!',
- 'files': r'changelog/.*(? too-much: foo, hello'
-def test_reinstall(tempdir_factory, store, log_info_mock):
+def test_reinstall(tempdir_factory, store, caplog):
path = make_repo(tempdir_factory, 'python_hooks_repo')
config = make_config_from_repo(path)
_get_hook(config, store, 'foo')
# We print some logging during clone (1) + install (3)
- assert log_info_mock.call_count == 4
- log_info_mock.reset_mock()
+ assert len(caplog.record_tuples) == 4
+ caplog.clear()
# Reinstall on another run should not trigger another install
_get_hook(config, store, 'foo')
- assert log_info_mock.call_count == 0
+ assert len(caplog.record_tuples) == 0
def test_control_c_control_c_on_install(tempdir_factory, store):
@@ -721,7 +241,7 @@ def test_control_c_control_c_on_install(tempdir_factory, store):
# raise as well.
with pytest.raises(MyKeyboardInterrupt):
with mock.patch.object(
- helpers, 'run_setup_cmd', side_effect=MyKeyboardInterrupt,
+ lang_base, 'setup_cmd', side_effect=MyKeyboardInterrupt,
):
with mock.patch.object(
shutil, 'rmtree', side_effect=MyKeyboardInterrupt,
@@ -730,7 +250,7 @@ def test_control_c_control_c_on_install(tempdir_factory, store):
# Should have made an environment, however this environment is broken!
hook, = hooks
- envdir = helpers.environment_dir(
+ envdir = lang_base.environment_dir(
hook.prefix,
python.ENVIRONMENT_DIR,
hook.language_version,
@@ -753,7 +273,7 @@ def test_invalidated_virtualenv(tempdir_factory, store):
hook = _get_hook(config, store, 'foo')
# Simulate breaking of the virtualenv
- envdir = helpers.environment_dir(
+ envdir = lang_base.environment_dir(
hook.prefix,
python.ENVIRONMENT_DIR,
hook.language_version,
@@ -835,13 +355,13 @@ def test_local_python_repo(store, local_python_config):
assert hook.language_version != C.DEFAULT
ret, out = _hook_run(hook, ('filename',), color=False)
assert ret == 0
- assert _norm_out(out) == b"['filename']\nHello World\n"
+ assert out == b"['filename']\nHello World\n"
def test_default_language_version(store, local_python_config):
config: dict[str, Any] = {
'default_language_version': {'python': 'fake'},
- 'default_stages': ['commit'],
+ 'default_stages': ['pre-commit'],
'repos': [local_python_config],
}
@@ -858,18 +378,18 @@ def test_default_language_version(store, local_python_config):
def test_default_stages(store, local_python_config):
config: dict[str, Any] = {
'default_language_version': {'python': C.DEFAULT},
- 'default_stages': ['commit'],
+ 'default_stages': ['pre-commit'],
'repos': [local_python_config],
}
# `stages` was not set, should default
hook, = all_hooks(config, store)
- assert hook.stages == ['commit']
+ assert hook.stages == ['pre-commit']
# `stages` is set, should not default
- config['repos'][0]['hooks'][0]['stages'] = ['push']
+ config['repos'][0]['hooks'][0]['stages'] = ['pre-push']
hook, = all_hooks(config, store)
- assert hook.stages == ['push']
+ assert hook.stages == ['pre-push']
def test_hook_id_not_present(tempdir_factory, store, caplog):
@@ -886,32 +406,6 @@ def test_hook_id_not_present(tempdir_factory, store, caplog):
)
-def test_too_new_version(tempdir_factory, store, caplog):
- path = make_repo(tempdir_factory, 'script_hooks_repo')
- with modify_manifest(path) as manifest:
- manifest[0]['minimum_pre_commit_version'] = '999.0.0'
- config = make_config_from_repo(path)
- with pytest.raises(SystemExit):
- _get_hook(config, store, 'bash_hook')
- _, msg = caplog.messages
- pattern = re_assert.Matches(
- r'^The hook `bash_hook` requires pre-commit version 999\.0\.0 but '
- r'version \d+\.\d+\.\d+ is installed. '
- r'Perhaps run `pip install --upgrade pre-commit`\.$',
- )
- pattern.assert_matches(msg)
-
-
-@pytest.mark.parametrize('version', ('0.1.0', C.VERSION))
-def test_versions_ok(tempdir_factory, store, version):
- path = make_repo(tempdir_factory, 'script_hooks_repo')
- with modify_manifest(path) as manifest:
- manifest[0]['minimum_pre_commit_version'] = version
- config = make_config_from_repo(path)
- # Should succeed
- _get_hook(config, store, 'bash_hook')
-
-
def test_manifest_hooks(tempdir_factory, store):
path = make_repo(tempdir_factory, 'script_hooks_repo')
config = make_config_from_repo(path)
@@ -930,18 +424,26 @@ def test_manifest_hooks(tempdir_factory, store):
exclude_types=[],
files='',
id='bash_hook',
- language='script',
+ language='unsupported_script',
language_version='default',
log_file='',
minimum_pre_commit_version='0',
name='Bash hook',
pass_filenames=True,
require_serial=False,
- stages=(
- 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg',
- 'post-commit', 'manual', 'post-checkout', 'push', 'post-merge',
+ stages=[
+ 'commit-msg',
+ 'post-checkout',
+ 'post-commit',
+ 'post-merge',
'post-rewrite',
- ),
+ 'pre-commit',
+ 'pre-merge-commit',
+ 'pre-push',
+ 'pre-rebase',
+ 'prepare-commit-msg',
+ 'manual',
+ ],
types=['file'],
types_or=[],
verbose=False,
@@ -949,29 +451,13 @@ def test_manifest_hooks(tempdir_factory, store):
)
-@pytest.mark.parametrize(
- 'repo',
- (
- 'dotnet_hooks_csproj_repo',
- 'dotnet_hooks_sln_repo',
- 'dotnet_hooks_combo_repo',
- 'dotnet_hooks_csproj_prefix_repo',
- ),
-)
-def test_dotnet_hook(tempdir_factory, store, repo):
- _test_hook_repo(
- tempdir_factory, store, repo,
- 'dotnet-example-hook', [], b'Hello from dotnet!\n',
- )
-
-
def test_non_installable_hook_error_for_language_version(store, caplog):
config = {
'repo': 'local',
'hooks': [{
'id': 'system-hook',
'name': 'system-hook',
- 'language': 'system',
+ 'language': 'unsupported',
'entry': 'python3 -c "import sys; print(sys.version)"',
'language_version': 'python3.10',
}],
@@ -983,7 +469,7 @@ def test_non_installable_hook_error_for_language_version(store, caplog):
msg, = caplog.messages
assert msg == (
'The hook `system-hook` specifies `language_version` but is using '
- 'language `system` which does not install an environment. '
+ 'language `unsupported` which does not install an environment. '
'Perhaps you meant to use a specific language?'
)
@@ -994,7 +480,7 @@ def test_non_installable_hook_error_for_additional_dependencies(store, caplog):
'hooks': [{
'id': 'system-hook',
'name': 'system-hook',
- 'language': 'system',
+ 'language': 'unsupported',
'entry': 'python3 -c "import sys; print(sys.version)"',
'additional_dependencies': ['astpretty'],
}],
@@ -1006,6 +492,28 @@ def test_non_installable_hook_error_for_additional_dependencies(store, caplog):
msg, = caplog.messages
assert msg == (
'The hook `system-hook` specifies `additional_dependencies` but is '
- 'using language `system` which does not install an environment. '
+ 'using language `unsupported` which does not install an environment. '
'Perhaps you meant to use a specific language?'
)
+
+
+def test_args_with_spaces_and_quotes(tmp_path):
+ ret = run_language(
+ tmp_path, unsupported,
+ f"{shlex.quote(sys.executable)} -c 'import sys; print(sys.argv[1:])'",
+ ('i have spaces', 'and"\'quotes', '$and !this'),
+ )
+
+ expected = b"['i have spaces', 'and\"\\'quotes', '$and !this']\n"
+ assert ret == (0, expected)
+
+
+def test_hazmat(tmp_path):
+ ret = run_language(
+ tmp_path, unsupported,
+ f'pre-commit hazmat ignore-exit-code {shlex.quote(sys.executable)} '
+ f"-c 'import sys; raise SystemExit(sys.argv[1:])'",
+ ('f1', 'f2'),
+ )
+ expected = b"['f1', 'f2']\n"
+ assert ret == (0, expected)
diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py
index a91f3151..cd2f6387 100644
--- a/tests/staged_files_only_test.py
+++ b/tests/staged_files_only_test.py
@@ -1,12 +1,15 @@
from __future__ import annotations
+import contextlib
import itertools
import os.path
import shutil
import pytest
+import re_assert
from pre_commit import git
+from pre_commit.errors import FatalError
from pre_commit.staged_files_only import staged_files_only
from pre_commit.util import cmd_output
from testing.auto_namedtuple import auto_namedtuple
@@ -14,6 +17,7 @@ from testing.fixtures import git_dir
from testing.util import cwd
from testing.util import get_resource_path
from testing.util import git_commit
+from testing.util import xfailif_windows
FOO_CONTENTS = '\n'.join(('1', '2', '3', '4', '5', '6', '7', '8', ''))
@@ -354,6 +358,21 @@ def test_crlf(in_git_dir, patch_dir, crlf_before, crlf_after, autocrlf):
assert_no_diff()
+@pytest.mark.parametrize('autocrlf', ('true', 'input'))
+def test_crlf_diff_only(in_git_dir, patch_dir, autocrlf):
+ # due to a quirk (?) in git -- a diff only in crlf does not show but
+ # still results in an exit code of `1`
+ # we treat this as "no diff" -- though ideally it would discard the diff
+ # while committing
+ cmd_output('git', 'config', '--local', 'core.autocrlf', autocrlf)
+
+ _write(b'1\r\n2\r\n3\r\n')
+ cmd_output('git', 'add', 'foo')
+ _write(b'1\n2\n3\n')
+ with staged_files_only(patch_dir):
+ pass
+
+
def test_whitespace_errors(in_git_dir, patch_dir):
cmd_output('git', 'config', '--local', 'apply.whitespace', 'error')
test_crlf(in_git_dir, patch_dir, True, True, 'true')
@@ -382,3 +401,49 @@ def test_intent_to_add(in_git_dir, patch_dir):
with staged_files_only(patch_dir):
assert_no_diff()
assert git.intent_to_add_files() == ['foo']
+
+
+@contextlib.contextmanager
+def _unreadable(f):
+ orig = os.stat(f).st_mode
+ os.chmod(f, 0o000)
+ try:
+ yield
+ finally:
+ os.chmod(f, orig)
+
+
+@xfailif_windows # pragma: win32 no cover
+def test_failed_diff_does_not_discard_changes(in_git_dir, patch_dir):
+ # stage 3 files
+ for i in range(3):
+ with open(str(i), 'w') as f:
+ f.write(str(i))
+ cmd_output('git', 'add', '0', '1', '2')
+
+ # modify all of their contents
+ for i in range(3):
+ with open(str(i), 'w') as f:
+ f.write('new contents')
+
+ with _unreadable('1'):
+ with pytest.raises(FatalError) as excinfo:
+ with staged_files_only(patch_dir):
+ raise AssertionError('should have errored on enter')
+
+ # the diff command failed to produce a diff of `1`
+ msg, = excinfo.value.args
+ re_assert.Matches(
+ r'^pre-commit failed to diff -- perhaps due to permissions\?\n\n'
+ r'command: .*\n'
+ r'return code: 128\n'
+ r'stdout: \(none\)\n'
+ r'stderr:\n'
+ r' error: open\("1"\): Permission denied\n'
+ r' fatal: cannot hash 1$',
+ ).assert_matches(msg)
+
+ # even though it errored, the unstaged changes should still be present
+ for i in range(3):
+ with open(str(i)) as f:
+ assert f.read() == 'new contents'
diff --git a/tests/store_test.py b/tests/store_test.py
index c42ce653..13f198ea 100644
--- a/tests/store_test.py
+++ b/tests/store_test.py
@@ -1,12 +1,15 @@
from __future__ import annotations
+import logging
import os.path
+import shlex
import sqlite3
import stat
from unittest import mock
import pytest
+import pre_commit.constants as C
from pre_commit import git
from pre_commit.store import _get_default_directory
from pre_commit.store import _LOCAL_RESOURCES
@@ -19,6 +22,17 @@ from testing.util import git_commit
from testing.util import xfailif_windows
+def _select_all_configs(store: Store) -> list[str]:
+ with store.connect() as db:
+ rows = db.execute('SELECT * FROM configs').fetchall()
+ return [path for path, in rows]
+
+
+def _select_all_repos(store: Store) -> list[tuple[str, str, str]]:
+ with store.connect() as db:
+ return db.execute('SELECT repo, ref, path FROM repos').fetchall()
+
+
def test_our_session_fixture_works():
"""There's a session fixture which makes `Store` invariantly raise to
prevent writing to the home directory.
@@ -65,7 +79,7 @@ def test_store_init(store):
assert text_line in readme_contents
-def test_clone(store, tempdir_factory, log_info_mock):
+def test_clone(store, tempdir_factory, caplog):
path = git_dir(tempdir_factory)
with cwd(path):
git_commit()
@@ -74,7 +88,7 @@ def test_clone(store, tempdir_factory, log_info_mock):
ret = store.clone(path, rev)
# Should have printed some stuff
- assert log_info_mock.call_args_list[0][0][0].startswith(
+ assert caplog.record_tuples[0][-1].startswith(
'Initializing environment for ',
)
@@ -88,7 +102,73 @@ def test_clone(store, tempdir_factory, log_info_mock):
assert git.head_rev(ret) == rev
# Assert there's an entry in the sqlite db for this
- assert store.select_all_repos() == [(path, rev, ret)]
+ assert _select_all_repos(store) == [(path, rev, ret)]
+
+
+def test_warning_for_deprecated_stages_on_init(store, tempdir_factory, caplog):
+ manifest = '''\
+- id: hook1
+ name: hook1
+ language: system
+ entry: echo hook1
+ stages: [commit, push]
+- id: hook2
+ name: hook2
+ language: system
+ entry: echo hook2
+ stages: [push, merge-commit]
+'''
+
+ path = git_dir(tempdir_factory)
+ with open(os.path.join(path, C.MANIFEST_FILE), 'w') as f:
+ f.write(manifest)
+ cmd_output('git', 'add', '.', cwd=path)
+ git_commit(cwd=path)
+ rev = git.head_rev(path)
+
+ store.clone(path, rev)
+ assert caplog.record_tuples[1] == (
+ 'pre_commit',
+ logging.WARNING,
+ f'repo `{path}` uses deprecated stage names '
+ f'(commit, push, merge-commit) which will be removed in a future '
+ f'version. '
+ f'Hint: often `pre-commit autoupdate --repo {shlex.quote(path)}` '
+ f'will fix this. '
+ f'if it does not -- consider reporting an issue to that repo.',
+ )
+
+ # should not re-warn
+ caplog.clear()
+ store.clone(path, rev)
+ assert caplog.record_tuples == []
+
+
+def test_no_warning_for_non_deprecated_stages_on_init(
+ store, tempdir_factory, caplog,
+):
+ manifest = '''\
+- id: hook1
+ name: hook1
+ language: system
+ entry: echo hook1
+ stages: [pre-commit, pre-push]
+- id: hook2
+ name: hook2
+ language: system
+ entry: echo hook2
+ stages: [pre-push, pre-merge-commit]
+'''
+
+ path = git_dir(tempdir_factory)
+ with open(os.path.join(path, C.MANIFEST_FILE), 'w') as f:
+ f.write(manifest)
+ cmd_output('git', 'add', '.', cwd=path)
+ git_commit(cwd=path)
+ rev = git.head_rev(path)
+
+ store.clone(path, rev)
+ assert logging.WARNING not in {tup[1] for tup in caplog.record_tuples}
def test_clone_cleans_up_on_checkout_failure(store):
@@ -118,7 +198,7 @@ def test_clone_when_repo_already_exists(store):
def test_clone_shallow_failure_fallback_to_complete(
store, tempdir_factory,
- log_info_mock,
+ caplog,
):
path = git_dir(tempdir_factory)
with cwd(path):
@@ -134,7 +214,7 @@ def test_clone_shallow_failure_fallback_to_complete(
ret = store.clone(path, rev)
# Should have printed some stuff
- assert log_info_mock.call_args_list[0][0][0].startswith(
+ assert caplog.record_tuples[0][-1].startswith(
'Initializing environment for ',
)
@@ -148,7 +228,7 @@ def test_clone_shallow_failure_fallback_to_complete(
assert git.head_rev(ret) == rev
# Assert there's an entry in the sqlite db for this
- assert store.select_all_repos() == [(path, rev, ret)]
+ assert _select_all_repos(store) == [(path, rev, ret)]
def test_clone_tag_not_on_mainline(store, tempdir_factory):
@@ -180,12 +260,12 @@ def test_create_when_store_already_exists(store):
def test_db_repo_name(store):
assert store.db_repo_name('repo', ()) == 'repo'
- assert store.db_repo_name('repo', ('b', 'a', 'c')) == 'repo:a,b,c'
+ assert store.db_repo_name('repo', ('b', 'a', 'c')) == 'repo:b,a,c'
def test_local_resources_reflects_reality():
on_disk = {
- res[len('empty_template_'):]
+ res.removeprefix('empty_template_')
for res in os.listdir('pre_commit/resources')
if res.startswith('empty_template_')
}
@@ -196,7 +276,7 @@ def test_mark_config_as_used(store, tmpdir):
with tmpdir.as_cwd():
f = tmpdir.join('f').ensure()
store.mark_config_used('f')
- assert store.select_all_configs() == [f.strpath]
+ assert _select_all_configs(store) == [f.strpath]
def test_mark_config_as_used_idempotent(store, tmpdir):
@@ -206,21 +286,12 @@ def test_mark_config_as_used_idempotent(store, tmpdir):
def test_mark_config_as_used_does_not_exist(store):
store.mark_config_used('f')
- assert store.select_all_configs() == []
-
-
-def _simulate_pre_1_14_0(store):
- with store.connect() as db:
- db.executescript('DROP TABLE configs')
-
-
-def test_select_all_configs_roll_forward(store):
- _simulate_pre_1_14_0(store)
- assert store.select_all_configs() == []
+ assert _select_all_configs(store) == []
def test_mark_config_as_used_roll_forward(store, tmpdir):
- _simulate_pre_1_14_0(store)
+ with store.connect() as db: # simulate pre-1.14.0
+ db.executescript('DROP TABLE configs')
test_mark_config_as_used(store, tmpdir)
@@ -245,4 +316,28 @@ def test_mark_config_as_used_readonly(tmpdir):
assert store.readonly
# should be skipped due to readonly
store.mark_config_used(str(cfg))
- assert store.select_all_configs() == []
+ assert _select_all_configs(store) == []
+
+
+def test_clone_with_recursive_submodules(store, tmp_path):
+ sub = tmp_path.joinpath('sub')
+ sub.mkdir()
+ sub.joinpath('submodule').write_text('i am a submodule')
+ cmd_output('git', '-C', str(sub), 'init', '.')
+ cmd_output('git', '-C', str(sub), 'add', '.')
+ git.commit(str(sub))
+
+ repo = tmp_path.joinpath('repo')
+ repo.mkdir()
+ repo.joinpath('repository').write_text('i am a repo')
+ cmd_output('git', '-C', str(repo), 'init', '.')
+ cmd_output('git', '-C', str(repo), 'add', '.')
+ cmd_output('git', '-C', str(repo), 'submodule', 'add', str(sub), 'sub')
+ git.commit(str(repo))
+
+ rev = git.head_rev(str(repo))
+ ret = store.clone(str(repo), rev)
+
+ assert os.path.exists(ret)
+ assert os.path.exists(os.path.join(ret, str(repo), 'repository'))
+ assert os.path.exists(os.path.join(ret, str(sub), 'submodule'))
diff --git a/tests/util_test.py b/tests/util_test.py
index 310f8f58..5b262113 100644
--- a/tests/util_test.py
+++ b/tests/util_test.py
@@ -16,7 +16,7 @@ from pre_commit.util import rmtree
def test_CalledProcessError_str():
- error = CalledProcessError(1, ('exe',), b'output', b'errors')
+ error = CalledProcessError(1, ('exe',), b'output\n', b'errors\n')
assert str(error) == (
"command: ('exe',)\n"
'return code: 1\n'
diff --git a/tests/xargs_test.py b/tests/xargs_test.py
index 0530e50d..e8000b25 100644
--- a/tests/xargs_test.py
+++ b/tests/xargs_test.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import concurrent.futures
+import multiprocessing
import os
import sys
import time
@@ -12,6 +13,40 @@ from pre_commit import parse_shebang
from pre_commit import xargs
+def test_cpu_count_sched_getaffinity_exists():
+ with mock.patch.object(
+ os, 'sched_getaffinity', create=True, return_value=set(range(345)),
+ ):
+ assert xargs.cpu_count() == 345
+
+
+@pytest.fixture
+def no_sched_getaffinity():
+ # Simulates an OS without os.sched_getaffinity available (mac/windows)
+ # https://docs.python.org/3/library/os.html#interface-to-the-scheduler
+ with mock.patch.object(
+ os,
+ 'sched_getaffinity',
+ create=True,
+ side_effect=AttributeError,
+ ):
+ yield
+
+
+def test_cpu_count_multiprocessing_cpu_count_implemented(no_sched_getaffinity):
+ with mock.patch.object(multiprocessing, 'cpu_count', return_value=123):
+ assert xargs.cpu_count() == 123
+
+
+def test_cpu_count_multiprocessing_cpu_count_not_implemented(
+ no_sched_getaffinity,
+):
+ with mock.patch.object(
+ multiprocessing, 'cpu_count', side_effect=NotImplementedError,
+ ):
+ assert xargs.cpu_count() == 1
+
+
@pytest.mark.parametrize(
('env', 'expected'),
(
@@ -147,6 +182,15 @@ def test_xargs_retcode_normal():
assert ret == 5
+@pytest.mark.xfail(sys.platform == 'win32', reason='posix only')
+def test_xargs_retcode_killed_by_signal():
+ ret, _ = xargs.xargs(
+ parse_shebang.normalize_cmd(('bash', '-c', 'kill -9 $$', '--')),
+ ('foo', 'bar'),
+ )
+ assert ret == -9
+
+
def test_xargs_concurrency():
bash_cmd = parse_shebang.normalize_cmd(('bash', '-c'))
print_pid = ('sleep 0.5 && echo $$',)
@@ -187,7 +231,7 @@ def test_xargs_propagate_kwargs_to_cmd():
assert b'Pre commit is awesome' in stdout
-@pytest.mark.xfail(os.name == 'nt', reason='posix only')
+@pytest.mark.xfail(sys.platform == 'win32', reason='posix only')
def test_xargs_color_true_makes_tty():
retcode, out = xargs.xargs(
(sys.executable, '-c', 'import sys; print(sys.stdout.isatty())'),
diff --git a/tests/yaml_rewrite_test.py b/tests/yaml_rewrite_test.py
new file mode 100644
index 00000000..d0f6841c
--- /dev/null
+++ b/tests/yaml_rewrite_test.py
@@ -0,0 +1,47 @@
+from __future__ import annotations
+
+import pytest
+
+from pre_commit.yaml import yaml_compose
+from pre_commit.yaml_rewrite import MappingKey
+from pre_commit.yaml_rewrite import MappingValue
+from pre_commit.yaml_rewrite import match
+from pre_commit.yaml_rewrite import SequenceItem
+
+
+def test_match_produces_scalar_values_only():
+ src = '''\
+- name: foo
+- name: [not, foo] # not a scalar: should be skipped!
+- name: bar
+'''
+ matcher = (SequenceItem(), MappingValue('name'))
+ ret = [n.value for n in match(yaml_compose(src), matcher)]
+ assert ret == ['foo', 'bar']
+
+
+@pytest.mark.parametrize('cls', (MappingKey, MappingValue))
+def test_mapping_not_a_map(cls):
+ m = cls('s')
+ assert list(m.match(yaml_compose('[foo]'))) == []
+
+
+def test_sequence_item_not_a_sequence():
+ assert list(SequenceItem().match(yaml_compose('s: val'))) == []
+
+
+def test_mapping_key():
+ m = MappingKey('s')
+ ret = [n.value for n in m.match(yaml_compose('s: val\nt: val2'))]
+ assert ret == ['s']
+
+
+def test_mapping_value():
+ m = MappingValue('s')
+ ret = [n.value for n in m.match(yaml_compose('s: val\nt: val2'))]
+ assert ret == ['val']
+
+
+def test_sequence_item():
+ ret = [n.value for n in SequenceItem().match(yaml_compose('[a, b, c]'))]
+ assert ret == ['a', 'b', 'c']
diff --git a/tox.ini b/tox.ini
index a44f93d4..609c2fe1 100644
--- a/tox.ini
+++ b/tox.ini
@@ -6,8 +6,8 @@ deps = -rrequirements-dev.txt
passenv = *
commands =
coverage erase
- coverage run -m pytest {posargs:tests}
- coverage report
+ coverage run -m pytest {posargs:tests} --ignore=tests/languages --durations=20
+ coverage report --omit=pre_commit/languages/*,tests/languages/*
[testenv:pre-commit]
skip_install = true