Compare commits

..

No commits in common. "main" and "v3.2.2" have entirely different histories.
main ... v3.2.2

109 changed files with 909 additions and 3043 deletions

View file

@ -6,4 +6,4 @@ runs:
using: composite
steps:
- uses: asottile/workflows/.github/actions/latest-git@v1.4.0
if: inputs.env == 'py39' && runner.os == 'Linux'
if: inputs.env == 'py38' && runner.os == 'Linux'

View file

@ -3,7 +3,7 @@ name: languages
on:
push:
branches: [main, test-me-*]
tags: '*'
tags:
pull_request:
concurrency:
@ -21,7 +21,7 @@ jobs:
fetch-depth: 0
- uses: actions/setup-python@v4
with:
python-version: '3.10'
python-version: 3.8
- name: install deps
run: python -mpip install -e . -r requirements-dev.txt
- name: vars
@ -36,10 +36,10 @@ jobs:
matrix:
include: ${{ fromJSON(needs.vars.outputs.languages) }}
steps:
- uses: asottile/workflows/.github/actions/fast-checkout@v1.8.1
- uses: asottile/workflows/.github/actions/fast-checkout@v1.4.0
- uses: actions/setup-python@v4
with:
python-version: '3.10'
python-version: 3.8
- run: echo "$CONDA\Scripts" >> "$GITHUB_PATH"
shell: bash
@ -63,10 +63,8 @@ jobs:
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'
- run: testing/get-swift.sh
if: matrix.os == 'ubuntu-latest' && matrix.language == 'swift'
- name: install deps
run: python -mpip install -e . -r requirements-dev.txt

View file

@ -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.8.1
uses: asottile/workflows/.github/workflows/tox.yml@v1.4.0
with:
env: '["py310"]'
env: '["py38"]'
os: windows-latest
main-linux:
uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1
uses: asottile/workflows/.github/workflows/tox.yml@v1.4.0
with:
env: '["py310", "py311", "py312", "py313"]'
env: '["py38", "py39", "py310"]'
os: ubuntu-latest

View file

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
@ -10,35 +10,36 @@ repos:
- id: name-tests-test
- id: requirements-txt-fixer
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v3.2.0
rev: v2.2.0
hooks:
- id: setup-cfg-fmt
- repo: https://github.com/asottile/reorder-python-imports
rev: v3.16.0
- repo: https://github.com/asottile/reorder_python_imports
rev: v3.9.0
hooks:
- id: reorder-python-imports
exclude: ^pre_commit/resources/
args: [--py310-plus, --add-import, 'from __future__ import annotations']
exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/)
args: [--py38-plus, --add-import, 'from __future__ import annotations']
- repo: https://github.com/asottile/add-trailing-comma
rev: v4.0.0
rev: v2.4.0
hooks:
- id: add-trailing-comma
args: [--py36-plus]
- repo: https://github.com/asottile/pyupgrade
rev: v3.21.2
rev: v3.3.1
hooks:
- id: pyupgrade
args: [--py310-plus]
- repo: https://github.com/hhatto/autopep8
rev: v2.3.2
args: [--py38-plus]
- repo: https://github.com/pre-commit/mirrors-autopep8
rev: v2.0.2
hooks:
- id: autopep8
- repo: https://github.com/PyCQA/flake8
rev: 7.3.0
rev: 6.0.0
hooks:
- id: flake8
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.19.1
rev: v1.1.1
hooks:
- id: mypy
additional_dependencies: [types-pyyaml]
additional_dependencies: [types-all]
exclude: ^testing/resources/

View file

@ -1,258 +1,3 @@
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
==================

View file

@ -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/lang_base.py`](https://github.com/pre-commit/pre-commit/blob/main/pre_commit/lang_base.py)
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)
#### `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 = lang_base.basic_default_version
get_default_version = helpers.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 = lang_base.basic_health_check
health_check = helpers.basic_healthy_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 = lang_base.no_install`
- (0th / 3rd class): `install_environment = helpers.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`

View file

@ -9,8 +9,6 @@ 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
@ -19,9 +17,9 @@ 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 unsupported
from pre_commit.languages import unsupported_script
from pre_commit.languages import system
languages: dict[str, Language] = {
@ -33,8 +31,6 @@ languages: dict[str, Language] = {
'dotnet': dotnet,
'fail': fail,
'golang': golang,
'haskell': haskell,
'julia': julia,
'lua': lua,
'node': node,
'perl': perl,
@ -43,8 +39,10 @@ languages: dict[str, Language] = {
'r': r,
'ruby': ruby,
'rust': rust,
'script': script,
'swift': swift,
'unsupported': unsupported,
'unsupported_script': unsupported_script,
'system': system,
# TODO: fully deprecate `python_venv`
'python_venv': python,
}
language_names = sorted(languages)

View file

@ -2,14 +2,12 @@ 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 NamedTuple
from typing import Sequence
import cfgv
from identify.identify import ALL_TAGS
@ -72,43 +70,6 @@ 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]
@ -117,12 +78,11 @@ class StagesMigrationNoDefault(NamedTuple):
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 = 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)
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:
@ -139,108 +99,13 @@ class StagesMigration(StagesMigrationNoDefault):
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),
LanguageMigrationRequired('language', cfgv.check_one_of(language_names)),
cfgv.Required('language', cfgv.check_one_of(language_names)),
cfgv.Optional('alias', cfgv.check_string, ''),
cfgv.Optional('files', check_string_regex, ''),
@ -259,6 +124,7 @@ 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),
StagesMigration('stages', []),
cfgv.Optional('verbose', cfgv.check_bool, False),
@ -270,19 +136,10 @@ 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=_load_manifest_forward_compat,
load_strategy=yaml_load,
exc_tp=InvalidManifestError,
)
@ -404,20 +261,12 @@ 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 `unsupported`
cfgv.Optional(
'language', cfgv.check_one_of({'unsupported'}), 'unsupported',
),
# language must be system
cfgv.Optional('language', cfgv.check_one_of({'system'}), 'system'),
# entry cannot be overridden
NotAllowed('entry', cfgv.check_any),
*(
@ -434,7 +283,6 @@ META_HOOK_DICT = cfgv.Map(
item
for item in MANIFEST_HOOK_DICT.items
),
*_COMMON_HOOK_WARNINGS,
)
CONFIG_HOOK_DICT = cfgv.Map(
'Hook', 'id',
@ -450,17 +298,18 @@ CONFIG_HOOK_DICT = cfgv.Map(
for item in MANIFEST_HOOK_DICT.items
if item.key != 'id'
if item.key != 'stages'
if item.key != 'language' # remove
),
StagesMigrationNoDefault('stages', []),
LanguageMigration('language', cfgv.check_one_of(language_names)), # remove
*_COMMON_HOOK_WARNINGS,
OptionalSensibleRegexAtHook('files', cfgv.check_string),
OptionalSensibleRegexAtHook('exclude', cfgv.check_string),
)
LOCAL_HOOK_DICT = cfgv.Map(
'Hook', 'id',
*MANIFEST_HOOK_DICT.items,
*_COMMON_HOOK_WARNINGS,
OptionalSensibleRegexAtHook('files', cfgv.check_string),
OptionalSensibleRegexAtHook('exclude', cfgv.check_string),
)
CONFIG_REPO_DICT = cfgv.Map(
'Repository', 'repo',
@ -496,13 +345,6 @@ DEFAULT_LANGUAGE_VERSION = cfgv.Map(
CONFIG_SCHEMA = cfgv.Map(
'Config', None,
# 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',
@ -513,10 +355,14 @@ CONFIG_SCHEMA = cfgv.Map(
'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.Optional(
'minimum_pre_commit_version',
cfgv.check_and(cfgv.check_string, check_min_version),
'0',
),
cfgv.WarnAdditionalKeys(
(
'repos',

View file

@ -1,23 +1,22 @@
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
@ -28,58 +27,49 @@ from pre_commit.yaml import yaml_load
class RevInfo(NamedTuple):
repo: str
rev: str
frozen: str | None = None
hook_ids: frozenset[str] = frozenset()
frozen: str | None
@classmethod
def from_config(cls, config: dict[str, Any]) -> RevInfo:
return cls(config['repo'], config['rev'])
return cls(config['repo'], config['rev'], None)
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, 'fetch', 'origin', 'HEAD',
'--quiet', '--filter=blob:none', '--tags',
*git_cmd, 'fetch', 'origin', 'HEAD', '--tags',
cwd=tmp,
)
try:
rev = cmd_output(*tag_cmd)[1].strip()
rev = cmd_output(*tag_cmd, cwd=tmp)[1].strip()
except CalledProcessError:
rev = cmd_output(*_git, 'rev-parse', 'FETCH_HEAD')[1].strip()
cmd = (*git_cmd, 'rev-parse', 'FETCH_HEAD')
rev = cmd_output(*cmd, cwd=tmp)[1].strip()
else:
if tags_only:
rev = git.get_best_candidate_tag(rev, tmp)
frozen = None
if freeze:
exact = cmd_output(*_git, 'rev-parse', rev)[1].strip()
exact_rev_cmd = (*git_cmd, 'rev-parse', rev)
exact = cmd_output(*exact_rev_cmd, cwd=tmp)[1].strip()
if exact != rev:
rev, frozen = exact, rev
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)
return self._replace(rev=rev, frozen=frozen)
class RepositoryCannotBeUpdatedError(RuntimeError):
@ -89,30 +79,24 @@ 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 - info.hook_ids
hooks_missing = hooks - {hook['id'] for hook in manifest}
if hooks_missing:
raise RepositoryCannotBeUpdatedError(
f'[{info.repo}] Cannot update because the update target is '
f'missing these hooks: {", ".join(sorted(hooks_missing))}',
f'Cannot update because the update target is missing these '
f'hooks:\n{", ".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)$')
@ -161,53 +145,49 @@ 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)
changed = False
retv = 0
rev_infos: list[RevInfo | None] = []
changed = False
config_repos = [
repo for repo in load_config(config_file)['repos']
if repo['repo'] not in {LOCAL, META}
]
config = load_config(config_file)
for repo_config in config['repos']:
if repo_config['repo'] in {LOCAL, META}:
continue
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
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)'
else:
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}')
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 changed:
_write_new_config(config_file, rev_infos)

View file

@ -12,7 +12,6 @@ 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(
@ -27,8 +26,7 @@ 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'])
@ -58,41 +56,34 @@ def _mark_used_repos(
))
def _gc(store: Store) -> int:
with store.exclusive_lock(), store.connect() as db:
store._create_configs_table(db)
def _gc_repos(store: Store) -> int:
configs = store.select_all_configs()
repos = store.select_all_repos()
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)
# 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)]
configs_rows = db.execute('SELECT path FROM configs').fetchall()
configs = [path for path, in configs_rows]
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)
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)
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)
def gc(store: Store) -> int:
output.write_line(f'{_gc(store)} repo(s) removed.')
with store.exclusive_lock():
repos_removed = _gc_repos(store)
output.write_line(f'{repos_removed} repo(s) removed.')
return 0

View file

@ -1,95 +0,0 @@
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())

View file

@ -4,7 +4,7 @@ import argparse
import os.path
import subprocess
import sys
from collections.abc import Sequence
from typing import Sequence
from pre_commit.commands.run import run
from pre_commit.envcontext import envcontext
@ -106,7 +106,6 @@ def _ns(
hook=None,
verbose=False,
show_diff_on_failure=False,
fail_fast=False,
)

View file

@ -103,7 +103,8 @@ def _install_hook_script(
hook_file.write(before + TEMPLATE_START)
hook_file.write(f'INSTALL_PYTHON={shlex.quote(sys.executable)}\n')
args_s = shlex.join(args)
# TODO: python3.8+: shlex.join
args_s = ' '.join(shlex.quote(part) for part in args)
hook_file.write(f'ARGS=({args_s})\n')
hook_file.write(TEMPLATE_END + after)
make_executable(hook_path)

View file

@ -1,21 +1,13 @@
from __future__ import annotations
import functools
import itertools
import re
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:
@ -46,69 +38,16 @@ def _migrate_map(contents: str) -> str:
return contents
def _preserve_style(n: ScalarNode, *, s: str) -> str:
style = n.style or ''
return f'{style}{s}{style}'
def _migrate_sha_to_rev(contents: str) -> str:
return re.sub(r'(\n\s+)sha:', r'\1rev:', contents)
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'),
def _migrate_python_venv(contents: str) -> str:
return re.sub(
r'(\n\s+)language: python_venv\b',
r'\1language: python',
contents,
)
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:
@ -123,7 +62,8 @@ def migrate_config(config_file: str, quiet: bool = False) -> int:
raise cfgv.ValidationError(str(e))
contents = _migrate_map(contents)
contents = _migrate_composed(contents)
contents = _migrate_sha_to_rev(contents)
contents = _migrate_python_venv(contents)
if contents != orig_contents:
with open(config_file, 'w') as f:

View file

@ -9,11 +9,10 @@ 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
@ -58,36 +57,37 @@ def _full_msg(
def filter_by_include_exclude(
names: Iterable[str],
names: Collection[str],
include: str,
exclude: str,
) -> Generator[str]:
) -> list[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: Iterable[str]) -> None:
def __init__(self, filenames: Collection[str]) -> None:
self.filenames = [f for f in filenames if os.path.lexists(f)]
@functools.cache
@functools.lru_cache(maxsize=None)
def _types_for_file(self, filename: str) -> set[str]:
return tags_from_path(filename)
def by_types(
self,
names: Iterable[str],
types: Iterable[str],
types_or: Iterable[str],
exclude_types: Iterable[str],
) -> Generator[str]:
names: Sequence[str],
types: Collection[str],
types_or: Collection[str],
exclude_types: Collection[str],
) -> list[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
):
yield filename
ret.append(filename)
return ret
def filenames_for_hook(self, hook: Hook) -> Generator[str]:
return self.by_types(
filter_by_include_exclude(
self.filenames,
hook.files,
hook.exclude,
),
def filenames_for_hook(self, hook: Hook) -> tuple[str, ...]:
names = self.filenames
names = filter_by_include_exclude(names, hook.files, hook.exclude)
names = self.by_types(
names,
hook.types,
hook.types_or,
hook.exclude_types,
)
return tuple(names)
@classmethod
def from_config(
cls,
filenames: Iterable[str],
filenames: Collection[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 = tuple(classifier.filenames_for_hook(hook))
filenames = 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.monotonic()
time_before = time.time()
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.monotonic() - time_before, 2) or 0
duration = round(time.time() - time_before, 2) or 0
diff_after = _get_diff()
# if the hook makes changes, fail the commit
@ -250,7 +250,7 @@ def _compute_cols(hooks: Sequence[Hook]) -> int:
return max(cols, 80)
def _all_filenames(args: argparse.Namespace) -> Iterable[str]:
def _all_filenames(args: argparse.Namespace) -> Collection[str]:
# these hooks do not operate on files
if args.hook_stage in {
'post-checkout', 'post-commit', 'post-merge', 'post-rewrite',
@ -298,8 +298,7 @@ def _run_hooks(
verbose=args.verbose, use_color=args.color,
)
retval |= current_retval
fail_fast = (config['fail_fast'] or hook.fail_fast or args.fail_fast)
if current_retval and fail_fast:
if retval and (config['fail_fast'] or hook.fail_fast):
break
if retval and args.show_diff_on_failure and prior_diff:
if args.all_files:

View file

@ -1,6 +1,6 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Sequence
from pre_commit import clientlib

View file

@ -1,6 +1,6 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Sequence
from pre_commit import clientlib

View file

@ -3,9 +3,10 @@ from __future__ import annotations
import contextlib
import enum
import os
from collections.abc import Generator
from collections.abc import MutableMapping
from typing import Generator
from typing import MutableMapping
from typing import NamedTuple
from typing import Tuple
from typing import Union
_Unset = enum.Enum('_Unset', 'UNSET')
@ -17,9 +18,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:
@ -33,7 +34,7 @@ def format_env(parts: SubstitutionT, env: MutableMapping[str, str]) -> str:
def envcontext(
patch: PatchesT,
_env: MutableMapping[str, str] | None = None,
) -> Generator[None]:
) -> Generator[None, None, None]:
"""In this context, `os.environ` is modified according to `patch`.
`patch` is an iterable of 2-tuples (key, value):

View file

@ -5,7 +5,7 @@ import functools
import os.path
import sys
import traceback
from collections.abc import Generator
from typing 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]:
def error_handler() -> Generator[None, None, None]:
try:
yield
except (Exception, KeyboardInterrupt) as e:

View file

@ -3,8 +3,8 @@ from __future__ import annotations
import contextlib
import errno
import sys
from collections.abc import Callable
from collections.abc import Generator
from typing import Callable
from typing 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]:
) -> Generator[None, None, 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]:
) -> Generator[None, None, 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]:
) -> Generator[None, None, None]:
with open(path, 'a+') as f:
with _locked(f.fileno(), blocked_cb):
yield

View file

@ -3,7 +3,7 @@ from __future__ import annotations
import logging
import os.path
import sys
from collections.abc import Mapping
from typing 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.warning(
logger.warn(
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'

View file

@ -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

View file

@ -1,23 +1,23 @@
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 ContextManager
from typing import Generator
from typing import NoReturn
from typing import Protocol
from typing import Sequence
import pre_commit.constants as C
from pre_commit import parse_shebang
from pre_commit 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
@ -128,7 +128,7 @@ def no_install(
@contextlib.contextmanager
def no_env(prefix: Prefix, version: str) -> Generator[None]:
def no_env(prefix: Prefix, version: str) -> Generator[None, None, None]:
yield
@ -140,7 +140,10 @@ def target_concurrency() -> int:
if 'TRAVIS' in os.environ:
return 2
else:
return xargs.cpu_count()
try:
return multiprocessing.cpu_count()
except NotImplementedError:
return 1
def _shuffled(seq: Sequence[str]) -> list[str]:
@ -168,14 +171,11 @@ def run_xargs(
# ordering.
file_args = _shuffled(file_args)
jobs = target_concurrency()
return xargs.xargs(cmd, file_args, target_concurrency=jobs, color=color)
return xargs(cmd, file_args, target_concurrency=jobs, color=color)
def hook_cmd(entry: str, args: Sequence[str]) -> tuple[str, ...]:
cmd = shlex.split(entry)
if cmd[:2] == ['pre-commit', 'hazmat']:
cmd = [sys.executable, '-m', 'pre_commit.commands.hazmat', *cmd[2:]]
return (*cmd, *args)
return (*shlex.split(entry), *args)
def basic_run_hook(

View file

@ -3,8 +3,8 @@ from __future__ import annotations
import contextlib
import os
import sys
from collections.abc import Generator
from collections.abc import Sequence
from typing import Generator
from typing import Sequence
from pre_commit import lang_base
from pre_commit.envcontext import envcontext
@ -41,7 +41,7 @@ def get_env_patch(env: str) -> PatchesT:
@contextlib.contextmanager
def in_env(prefix: Prefix, version: str) -> Generator[None]:
def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir)):
yield

View file

@ -2,8 +2,8 @@ from __future__ import annotations
import contextlib
import os.path
from collections.abc import Generator
from collections.abc import Sequence
from typing import Generator
from typing import Sequence
from pre_commit import lang_base
from pre_commit.envcontext import envcontext
@ -70,7 +70,7 @@ def get_env_patch(target_dir: str) -> PatchesT:
@contextlib.contextmanager
def in_env(prefix: Prefix, version: str) -> Generator[None]:
def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir)):
yield

View file

@ -4,8 +4,8 @@ import contextlib
import os.path
import shutil
import tempfile
from collections.abc import Generator
from collections.abc import Sequence
from typing import Generator
from typing import Sequence
from pre_commit import lang_base
from pre_commit.envcontext import envcontext
@ -29,7 +29,7 @@ def get_env_patch(venv: str) -> PatchesT:
@contextlib.contextmanager
def in_env(prefix: Prefix, version: str) -> Generator[None]:
def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir)):
yield

View file

@ -1,12 +1,9 @@
from __future__ import annotations
import contextlib
import functools
import hashlib
import json
import os
import re
from collections.abc import Sequence
from typing import Sequence
from pre_commit import lang_base
from pre_commit.prefix import Prefix
@ -19,34 +16,32 @@ 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()
return None
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.')
def _get_docker_path(path: str) -> str:
container_id = _get_container_id()
if container_id is None:
if not _is_in_docker():
return path
container_id = _get_container_id()
try:
_, out, _ = cmd_output_b('docker', 'inspect', container_id)
except CalledProcessError:
@ -106,47 +101,17 @@ 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 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
def docker_cmd() -> 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
@ -174,7 +139,7 @@ def run_hook(
entry_tag = ('--entrypoint', entry_exe, docker_tag(prefix))
return lang_base.run_xargs(
(*docker_cmd(color=color), *entry_tag, *cmd_rest),
(*docker_cmd(), *entry_tag, *cmd_rest),
file_args,
require_serial=require_serial,
color=color,

View file

@ -1,6 +1,6 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Sequence
from pre_commit import lang_base
from pre_commit.languages.docker import docker_cmd
@ -23,7 +23,7 @@ def run_hook(
require_serial: bool,
color: bool,
) -> tuple[int, bytes]: # pragma: win32 no cover
cmd = docker_cmd(color=color) + lang_base.hook_cmd(entry, args)
cmd = docker_cmd() + lang_base.hook_cmd(entry, args)
return lang_base.run_xargs(
cmd,
file_args,

View file

@ -6,8 +6,8 @@ import re
import tempfile
import xml.etree.ElementTree
import zipfile
from collections.abc import Generator
from collections.abc import Sequence
from typing import Generator
from typing import Sequence
from pre_commit import lang_base
from pre_commit.envcontext import envcontext
@ -30,14 +30,14 @@ def get_env_patch(venv: str) -> PatchesT:
@contextlib.contextmanager
def in_env(prefix: Prefix, version: str) -> Generator[None]:
def in_env(prefix: Prefix, version: str) -> Generator[None, None, 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]:
def _nuget_config_no_sources() -> Generator[str, None, None]:
with tempfile.TemporaryDirectory() as tmpdir:
nuget_config = os.path.join(tmpdir, 'nuget.config')
with open(nuget_config, 'w') as f:

View file

@ -1,6 +1,6 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Sequence
from pre_commit import lang_base
from pre_commit.prefix import Prefix

View file

@ -12,18 +12,17 @@ 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.git import no_git_env
from pre_commit.prefix import Prefix
from pre_commit.util import cmd_output
from pre_commit.util import rmtree
@ -75,7 +74,6 @@ 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,
@ -90,7 +88,8 @@ def _infer_go_version(version: str) -> str:
if version != C.DEFAULT:
return version
resp = urllib.request.urlopen('https://go.dev/dl/?mode=json')
return json.load(resp)[0]['version'].removeprefix('go')
# TODO: 3.9+ .removeprefix('go')
return json.load(resp)[0]['version'][2:]
def _get_url(version: str) -> str:
@ -121,7 +120,7 @@ def _install_go(version: str, dest: str) -> None:
@contextlib.contextmanager
def in_env(prefix: Prefix, version: str) -> Generator[None]:
def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir, version)):
yield
@ -142,10 +141,9 @@ def install_environment(
else:
gopath = env_dir
env = no_git_env(dict(os.environ, GOPATH=gopath))
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'],

View file

@ -1,56 +0,0 @@
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,
),
)

View file

@ -1,133 +0,0 @@
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,
)

View file

@ -3,8 +3,8 @@ from __future__ import annotations
import contextlib
import os
import sys
from collections.abc import Generator
from collections.abc import Sequence
from typing import Generator
from typing import Sequence
from pre_commit import lang_base
from pre_commit.envcontext import envcontext
@ -44,7 +44,7 @@ def get_env_patch(d: str) -> PatchesT: # pragma: win32 no cover
@contextlib.contextmanager # pragma: win32 no cover
def in_env(prefix: Prefix, version: str) -> Generator[None]:
def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir)):
yield

View file

@ -4,8 +4,8 @@ import contextlib
import functools
import os
import sys
from collections.abc import Generator
from collections.abc import Sequence
from typing import Generator
from typing import Sequence
import pre_commit.constants as C
from pre_commit import lang_base
@ -59,7 +59,7 @@ def get_env_patch(venv: str) -> PatchesT:
@contextlib.contextmanager
def in_env(prefix: Prefix, version: str) -> Generator[None]:
def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir)):
yield
@ -93,7 +93,7 @@ def install_environment(
# install as if we installed from git
local_install_cmd = (
'npm', 'install', '--include=dev', '--include=prod',
'npm', 'install', '--dev', '--prod',
'--ignore-prepublish', '--no-progress', '--no-save',
)
lang_base.setup_cmd(prefix, local_install_cmd)

View file

@ -3,8 +3,8 @@ from __future__ import annotations
import contextlib
import os
import shlex
from collections.abc import Generator
from collections.abc import Sequence
from typing import Generator
from typing import Sequence
from pre_commit import lang_base
from pre_commit.envcontext import envcontext
@ -33,7 +33,7 @@ def get_env_patch(venv: str) -> PatchesT:
@contextlib.contextmanager
def in_env(prefix: Prefix, version: str) -> Generator[None]:
def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir)):
yield

View file

@ -3,9 +3,9 @@ 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

View file

@ -4,8 +4,8 @@ import contextlib
import functools
import os
import sys
from collections.abc import Generator
from collections.abc import Sequence
from typing import Generator
from typing import Sequence
import pre_commit.constants as C
from pre_commit import lang_base
@ -24,7 +24,7 @@ ENVIRONMENT_DIR = 'py_env'
run_hook = lang_base.basic_run_hook
@functools.cache
@functools.lru_cache(maxsize=None)
def _version_info(exe: str) -> str:
prog = 'import sys;print(".".join(str(p) for p in sys.version_info))'
try:
@ -65,7 +65,7 @@ def _find_by_py_launcher(
version: str,
) -> str | None: # pragma: no cover (windows only)
if version.startswith('python'):
num = version.removeprefix('python')
num = version[len('python'):]
cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)')
env = dict(os.environ, PYTHONIOENCODING='UTF-8')
try:
@ -75,13 +75,6 @@ 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())
@ -107,25 +100,18 @@ def _find_by_sys_executable() -> str | None:
@functools.lru_cache(maxsize=1)
def get_default_version() -> str: # pragma: no cover (platform dependent)
v_major = f'{sys.version_info[0]}'
v_minor = f'{sys.version_info[0]}.{sys.version_info[1]}'
# First attempt from `sys.executable` (or the realpath)
exe = _find_by_sys_executable()
if 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
# Next try the `pythonX.X` executable
exe = f'python{sys.version_info[0]}.{sys.version_info[1]}'
if find_executable(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
if _find_by_py_launcher(exe):
return exe
# We tried!
return C.DEFAULT
@ -138,7 +124,7 @@ def _sys_executable_matches(version: str) -> bool:
return False
try:
info = tuple(int(p) for p in version.removeprefix('python').split('.'))
info = tuple(int(p) for p in version[len('python'):].split('.'))
except ValueError:
return False
@ -166,7 +152,7 @@ def norm_version(version: str) -> str | None:
@contextlib.contextmanager
def in_env(prefix: Prefix, version: str) -> Generator[None]:
def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir)):
yield

View file

@ -4,120 +4,21 @@ import contextlib
import os
import shlex
import shutil
import tempfile
import textwrap
from collections.abc import Generator
from collections.abc import Sequence
from typing import Generator
from typing import Sequence
from pre_commit import lang_base
from pre_commit.envcontext import envcontext
from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import UNSET
from pre_commit.prefix import Prefix
from pre_commit.util import cmd_output
from pre_commit.util import cmd_output_b
from pre_commit.util import win_exe
ENVIRONMENT_DIR = 'renv'
RSCRIPT_OPTS = ('--no-save', '--no-restore', '--no-site-file', '--no-environ')
get_default_version = 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
health_check = lang_base.basic_health_check
def get_env_patch(venv: str) -> PatchesT:
@ -128,7 +29,7 @@ def get_env_patch(venv: str) -> PatchesT:
@contextlib.contextmanager
def in_env(prefix: Prefix, version: str) -> Generator[None]:
def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir)):
yield
@ -184,7 +85,7 @@ def _cmd_from_hook(
_entry_validate(cmd)
cmd_part = _prefix_if_file_entry(cmd, prefix, is_local=is_local)
return (cmd[0], *_RENV_ACTIVATED_OPTS, *cmd_part, *args)
return (cmd[0], *RSCRIPT_OPTS, *cmd_part, *args)
def install_environment(
@ -192,8 +93,6 @@ def install_environment(
version: str,
additional_dependencies: Sequence[str],
) -> None:
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)
@ -227,19 +126,21 @@ def install_environment(
renv::install(prefix_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)
cmd_output_b(
_rscript_exec(), '--vanilla', '-e',
_inline_r_setup(r_code_inst_environment),
cwd=env_dir,
)
if additional_dependencies:
r_code_inst_add = 'renv::install(commandArgs(trailingOnly = TRUE))'
_execute_r_in_renv(
code=r_code_inst_add, prefix=prefix, version=version,
args=additional_dependencies,
cwd=env_dir,
)
with in_env(prefix, version):
cmd_output_b(
_rscript_exec(), *RSCRIPT_OPTS, '-e',
_inline_r_setup(r_code_inst_add),
*additional_dependencies,
cwd=env_dir,
)
def _inline_r_setup(code: str) -> str:
@ -247,16 +148,11 @@ 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 = [
textwrap.dedent("""\
options(
install.packages.compile.from.source = "never",
pkgType = "binary"
)
"""),
code,
]
return '\n'.join(with_option)
with_option = f"""\
options(install.packages.compile.from.source = "never", pkgType = "binary")
{code}
"""
return with_option
def run_hook(

View file

@ -6,9 +6,9 @@ import importlib.resources
import os.path
import shutil
import tarfile
from collections.abc import Generator
from collections.abc import Sequence
from typing import Generator
from typing import IO
from typing import Sequence
import pre_commit.constants as C
from pre_commit import lang_base
@ -25,8 +25,7 @@ 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')
return importlib.resources.open_binary('pre_commit.resources', filename)
@functools.lru_cache(maxsize=1)
@ -73,7 +72,7 @@ def get_env_patch(
@contextlib.contextmanager
def in_env(prefix: Prefix, version: str) -> Generator[None]:
def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir, version)):
yield
@ -115,8 +114,6 @@ def _install_ruby(
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):
@ -138,8 +135,6 @@ def install_environment(
'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,
),
)

View file

@ -7,8 +7,8 @@ import shutil
import sys
import tempfile
import urllib.request
from collections.abc import Generator
from collections.abc import Sequence
from typing import Generator
from typing import Sequence
import pre_commit.constants as C
from pre_commit import lang_base
@ -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, cwd='/')[0] == 0:
if cmd_output_b('cargo', '--version', check=False)[0] == 0:
return 'system'
else:
return C.DEFAULT
@ -61,7 +61,7 @@ def get_env_patch(target_dir: str, version: str) -> PatchesT:
@contextlib.contextmanager
def in_env(prefix: Prefix, version: str) -> Generator[None]:
def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir, version)):
yield
@ -134,7 +134,7 @@ def install_environment(
packages_to_install: set[tuple[str, ...]] = {('--path', '.')}
for cli_dep in cli_deps:
cli_dep = cli_dep.removeprefix('cli:')
cli_dep = cli_dep[len('cli:'):]
package, _, crate_version = cli_dep.partition(':')
if crate_version != '':
packages_to_install.add((package, '--version', crate_version))

View file

@ -1,6 +1,6 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Sequence
from pre_commit import lang_base
from pre_commit.prefix import Prefix

View file

@ -2,8 +2,8 @@ from __future__ import annotations
import contextlib
import os
from collections.abc import Generator
from collections.abc import Sequence
from typing import Generator
from typing import Sequence
from pre_commit import lang_base
from pre_commit.envcontext import envcontext
@ -27,7 +27,7 @@ def get_env_patch(venv: str) -> PatchesT: # pragma: win32 no cover
@contextlib.contextmanager # pragma: win32 no cover
def in_env(prefix: Prefix, version: str) -> Generator[None]:
def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir)):
yield

View file

@ -2,7 +2,7 @@ from __future__ import annotations
import contextlib
import logging
from collections.abc import Generator
from typing 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]:
def logging_handler(use_color: bool) -> Generator[None, None, None]:
handler = LoggingHandler(use_color)
logger.addHandler(handler)
logger.setLevel(logging.INFO)

View file

@ -4,13 +4,12 @@ import argparse
import logging
import os
import sys
from collections.abc import Sequence
from typing import Sequence
import pre_commit.constants as C
from pre_commit import clientlib
from pre_commit import git
from pre_commit.color import add_color_option
from pre_commit.commands import hazmat
from pre_commit.commands.autoupdate import autoupdate
from pre_commit.commands.clean import clean
from pre_commit.commands.gc import gc
@ -38,11 +37,8 @@ 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', 'hazmat', 'init-templatedir', 'sample-config',
'clean', 'gc', 'init-templatedir', 'sample-config',
'validate-config', 'validate-manifest',
}
@ -63,10 +59,10 @@ def _add_hook_type_option(parser: argparse.ArgumentParser) -> None:
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')
parser.add_argument('--verbose', '-v', action='store_true', default=False)
mutex_group = parser.add_mutually_exclusive_group(required=False)
mutex_group.add_argument(
'--all-files', '-a', action='store_true',
'--all-files', '-a', action='store_true', default=False,
help='Run on all the files in the repo.',
)
mutex_group.add_argument(
@ -77,10 +73,6 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None:
'--show-diff-on-failure', action='store_true',
help='When hooks fail, run `git diff` directly afterward.',
)
parser.add_argument(
'--fail-fast', action='store_true',
help='Stop after the first failing hook.',
)
parser.add_argument(
'--hook-stage',
choices=clientlib.STAGES,
@ -234,23 +226,14 @@ 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', default=[],
'--repo', dest='repos', action='append', metavar='REPO',
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=(
@ -285,7 +268,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',
'--allow-missing-config', action='store_true', default=False,
help=(
'Whether to allow a missing `pre-commit` configuration file '
'or exit with a failure code.'
@ -385,18 +368,15 @@ def main(argv: Sequence[str] | None = None) -> int:
if args.command == 'autoupdate':
return autoupdate(
args.config,
args.config, store,
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,

View file

@ -1,7 +1,7 @@
from __future__ import annotations
import argparse
from collections.abc import Sequence
from typing 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 any(classifier.filenames_for_hook(hook)):
elif not classifier.filenames_for_hook(hook):
print(f'{hook.id} does not apply to this repository')
retv = 1

View file

@ -2,8 +2,7 @@ from __future__ import annotations
import argparse
import re
from collections.abc import Iterable
from collections.abc import Sequence
from typing import Sequence
from cfgv import apply_defaults
@ -15,7 +14,7 @@ from pre_commit.commands.run import Classifier
def exclude_matches_any(
filenames: Iterable[str],
filenames: Sequence[str],
include: str,
exclude: str,
) -> bool:
@ -51,12 +50,11 @@ 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.by_types(
classifier.filenames,
hook['types'],
hook['types_or'],
hook['exclude_types'],
)
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)
include, exclude = hook['files'], hook['exclude']
if not exclude_matches_any(names, include, exclude):
print(

View file

@ -1,7 +1,7 @@
from __future__ import annotations
import sys
from collections.abc import Sequence
from typing import Sequence
from pre_commit import output

View file

@ -1,7 +1,7 @@
from __future__ import annotations
import os.path
from collections.abc import Mapping
from typing import Mapping
from typing import NoReturn
from identify.identify import parse_shebang_from_file

View file

@ -3,14 +3,16 @@ from __future__ import annotations
import json
import logging
import os
from collections.abc import Sequence
import shlex
from typing import Any
from typing import Sequence
import pre_commit.constants as C
from pre_commit.all_languages import languages
from pre_commit.clientlib import load_manifest
from pre_commit.clientlib import LOCAL
from pre_commit.clientlib import META
from pre_commit.clientlib import parse_version
from pre_commit.hook import Hook
from pre_commit.lang_base import environment_dir
from pre_commit.prefix import Prefix
@ -67,6 +69,14 @@ def _hook_install(hook: Hook) -> None:
logger.info('Once installed this environment will be reused.')
logger.info('This may take a few minutes...')
if hook.language == 'python_venv':
logger.warning(
f'`repo: {hook.src}` uses deprecated `language: python_venv`. '
f'This is an alias for `language: python`. '
f'Often `pre-commit autoupdate --repo {shlex.quote(hook.src)}` '
f'will fix this.',
)
lang = languages[hook.language]
assert lang.ENVIRONMENT_DIR is not None
@ -114,6 +124,15 @@ 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]

View file

@ -1,4 +1,4 @@
name: pre_commit_empty_pubspec
environment:
sdk: '>=2.12.0'
sdk: '>=2.10.0'
executables: {}

View file

@ -1,4 +1,4 @@
from setuptools import setup
setup(name='pre-commit-placeholder-package', version='0.0.0', py_modules=[])
setup(name='pre-commit-placeholder-package', version='0.0.0')

Binary file not shown.

View file

@ -4,7 +4,7 @@ import contextlib
import logging
import os.path
import time
from collections.abc import Generator
from typing import Generator
from pre_commit import git
from pre_commit.errors import FatalError
@ -33,7 +33,7 @@ def _git_apply(patch: str) -> None:
@contextlib.contextmanager
def _intent_to_add_cleared() -> Generator[None]:
def _intent_to_add_cleared() -> Generator[None, None, None]:
intent_to_add = git.intent_to_add_files()
if intent_to_add:
logger.warning('Unstaged intent-to-add files detected.')
@ -48,7 +48,7 @@ def _intent_to_add_cleared() -> Generator[None]:
@contextlib.contextmanager
def _unstaged_changes_cleared(patch_dir: str) -> Generator[None]:
def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]:
tree = cmd_output('git', 'write-tree')[1].strip()
diff_cmd = (
'git', 'diff-index', '--ignore-submodules', '--binary',
@ -59,11 +59,6 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None]:
# 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)
@ -105,7 +100,7 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None]:
@contextlib.contextmanager
def staged_files_only(patch_dir: str) -> Generator[None]:
def staged_files_only(patch_dir: str) -> Generator[None, None, None]:
"""Clear any unstaged changes from the git working directory inside this
context.
"""

View file

@ -5,18 +5,18 @@ import logging
import os.path
import sqlite3
import tempfile
from collections.abc import Callable
from collections.abc import Generator
from collections.abc import Sequence
from typing import Callable
from typing import Generator
from typing 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_configs_table(db)
self._create_config_table(db)
# Atomic file move
os.replace(tmpfile, self.db_path)
@contextlib.contextmanager
def exclusive_lock(self) -> Generator[None]:
def exclusive_lock(self) -> Generator[None, None, None]:
def blocked_cb() -> None: # pragma: no cover (tests are in-process)
logger.info('Locking pre-commit directory')
@ -112,7 +112,7 @@ class Store:
def connect(
self,
db_path: str | None = None,
) -> Generator[sqlite3.Connection]:
) -> Generator[sqlite3.Connection, None, None]:
db_path = db_path or self.db_path
# sqlite doesn't close its fd with its contextmanager >.<
# contextlib.closing fixes this.
@ -136,7 +136,6 @@ 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:
@ -169,9 +168,6 @@ 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:
@ -214,7 +210,7 @@ class Store:
'local', C.LOCAL_REPO_VERSION, deps, _make_local_repo,
)
def _create_configs_table(self, db: sqlite3.Connection) -> None:
def _create_config_table(self, db: sqlite3.Connection) -> None:
db.executescript(
'CREATE TABLE IF NOT EXISTS configs ('
' path TEXT NOT NULL,'
@ -231,5 +227,28 @@ class Store:
return
with self.connect() as db:
# TODO: eventually remove this and only create in _create
self._create_configs_table(db)
self._create_config_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)

View file

@ -8,10 +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 pre_commit import parse_shebang
@ -25,7 +25,7 @@ def force_bytes(exc: Any) -> bytes:
@contextlib.contextmanager
def clean_path_on_failure(path: str) -> Generator[None]:
def clean_path_on_failure(path: str) -> Generator[None, None, None]:
"""Cleans up the directory on an exceptional failure."""
try:
yield
@ -36,8 +36,7 @@ def clean_path_on_failure(path: str) -> Generator[None]:
def resource_text(filename: str) -> str:
files = importlib.resources.files('pre_commit.resources')
return files.joinpath(filename).read_text()
return importlib.resources.read_text('pre_commit.resources', filename)
def make_executable(filename: str) -> None:
@ -202,37 +201,24 @@ else: # pragma: no cover
cmd_output_p = cmd_output_b
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],
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],
) -> None:
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)
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)
def win_exe(s: str) -> str:

View file

@ -3,16 +3,15 @@ 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
@ -23,21 +22,6 @@ 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`
@ -120,6 +104,7 @@ 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
@ -177,8 +162,7 @@ def xargs(
results = thread_map(run_cmd_partition, partitions)
for proc_retcode, proc_out, _ in results:
if abs(proc_retcode) > abs(retcode):
retcode = proc_retcode
retcode = max(retcode, proc_retcode)
stdout += proc_out
return retcode, stdout

View file

@ -6,7 +6,6 @@ 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)

View file

@ -1,52 +0,0 @@
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))

View file

@ -1,6 +1,6 @@
[metadata]
name = pre_commit
version = 4.5.1
version = 3.2.2
description = A framework for managing and maintaining multi-language pre-commit hooks.
long_description = file: README.md
long_description_content_type = text/markdown
@ -8,8 +8,9 @@ url = https://github.com/pre-commit/pre-commit
author = Anthony Sottile
author_email = asottile@umich.edu
license = MIT
license_files = LICENSE
license_file = LICENSE
classifiers =
License :: OSI Approved :: MIT License
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: Implementation :: CPython
@ -23,7 +24,7 @@ install_requires =
nodeenv>=0.11.1
pyyaml>=5.1
virtualenv>=20.10.0
python_requires = >=3.10
python_requires = >=3.8
[options.packages.find]
exclude =
@ -52,7 +53,6 @@ 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

View file

@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION=2.19.6
VERSION=2.13.4
if [ "$OSTYPE" = msys ]; then
URL="https://storage.googleapis.com/dart-archive/channels/stable/release/${VERSION}/sdk/dartsdk-windows-x64-release.zip"

29
testing/get-swift.sh Executable file
View file

@ -0,0 +1,29 @@
#!/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"

View file

@ -1,7 +1,7 @@
from __future__ import annotations
import os
from collections.abc import Sequence
from typing import Sequence
from pre_commit.lang_base import Language
from pre_commit.prefix import Prefix

View file

@ -16,15 +16,6 @@ EXCLUDED = frozenset((
))
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
@ -56,14 +47,10 @@ def main() -> int:
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
lang: files
for lang, files in zip(langs, exe.map(_lang_files, langs))
}

View file

@ -8,7 +8,7 @@ import shutil
import subprocess
import tarfile
import tempfile
from collections.abc import Sequence
from typing import Sequence
# This is a script for generating the tarred resources for git repo
@ -16,8 +16,8 @@ from collections.abc import Sequence
REPOS = (
('rbenv', 'https://github.com/rbenv/rbenv', '10e96bfc'),
('ruby-build', 'https://github.com/rbenv/ruby-build', '447468b1'),
('rbenv', 'https://github.com/rbenv/rbenv', '38e1fbb'),
('ruby-build', 'https://github.com/rbenv/ruby-build', '9d92a69'),
(
'ruby-download',
'https://github.com/garnieretienne/rvm-download',
@ -57,7 +57,8 @@ def make_archive(name: str, repo: str, ref: str, destdir: str) -> str:
arcs.sort()
with gzip.GzipFile(output_path, 'wb', mtime=0) as gzipf:
with tarfile.open(fileobj=gzipf, mode='w') as tf:
# https://github.com/python/typeshed/issues/5491
with tarfile.open(fileobj=gzipf, mode='w') as tf: # type: ignore
for arcname, abspath in arcs:
tf.add(
abspath,

View file

@ -0,0 +1,6 @@
- id: python3-hook
name: Python 3 Hook
entry: python3-hook
language: python
language_version: python3
files: \.py$

View file

@ -0,0 +1,8 @@
import sys
def main():
print(sys.version_info[0])
print(repr(sys.argv[1:]))
print('Hello World')
return 0

View file

@ -0,0 +1,8 @@
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']},
)

View file

@ -0,0 +1,5 @@
- id: system-hook-with-spaces
name: System hook with spaces
entry: bash -c 'echo "Hello World"'
language: system
files: \.sh$

View file

@ -40,7 +40,6 @@ def run_opts(
color=False,
verbose=False,
hook=None,
fail_fast=False,
remote_branch='',
local_branch='',
from_ref='',
@ -66,7 +65,6 @@ 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,

View file

@ -1,4 +1,4 @@
FROM ubuntu:jammy
FROM ubuntu:focal
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 distlib no-manylinux --upgrade
&& pip install --no-cache-dir pip setuptools wheel no-manylinux --upgrade

View file

@ -4,6 +4,7 @@ from __future__ import annotations
import argparse
import base64
import hashlib
import importlib.resources
import io
import os.path
import shutil
@ -41,17 +42,10 @@ def _add_shim(dest: str) -> None:
with zipfile.ZipFile(bio, 'w') as zipf:
zipf.write(shim, arcname='__main__.py')
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())
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())
def _write_cache_key(version: str, wheeldir: str, dest: str) -> None:
@ -107,6 +101,9 @@ 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

View file

@ -0,0 +1,7 @@
from __future__ import annotations
from pre_commit.all_languages import languages
def test_python_venv_is_an_alias_to_python():
assert languages['python_venv'] is languages['python']

View file

@ -12,8 +12,6 @@ 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
@ -42,51 +40,56 @@ def test_check_type_tag_success():
@pytest.mark.parametrize(
'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'],
},
],
}],
},
('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,
),
),
)
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_config_valid(config_obj, expected):
ret = is_valid_according_to_schema(config_obj, CONFIG_SCHEMA)
assert ret is expected
def test_local_hooks_with_rev_fails():
@ -195,13 +198,14 @@ def test_warn_mutable_rev_conditional():
),
)
def test_sensible_regex_validators_dont_pass_none(validator_cls):
validator = validator_cls('files', cfgv.check_string)
key = 'files'
with pytest.raises(cfgv.ValidationError) as excinfo:
validator.check({'files': None})
validator = validator_cls(key, cfgv.check_string)
validator.check({key: None})
assert str(excinfo.value) == (
'\n'
'==> At key: files'
f'==> At key: {key}'
'\n'
'=====> Expected string got NoneType'
)
@ -258,24 +262,6 @@ 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'),
(
@ -311,128 +297,47 @@ 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',
('manifest_obj', 'expected'),
(
[{
'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,
}],
(
[{
'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,
),
),
)
def test_valid_manifests(manifest_obj):
assert is_valid_according_to_schema(manifest_obj, MANIFEST_SCHEMA)
def test_valid_manifests(manifest_obj, expected):
ret = is_valid_according_to_schema(manifest_obj, MANIFEST_SCHEMA)
assert ret is expected
@pytest.mark.parametrize(
@ -488,39 +393,8 @@ 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'
@ -590,18 +464,3 @@ def test_config_hook_stages_defaulting():
'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`.'
)

View file

@ -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)._replace(hook_ids=frozenset(('foo',)))
info = RevInfo.from_config(config)
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):
def test_autoupdate_up_to_date_repo(up_to_date, tmpdir, store):
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):
cfg = tmpdir.join(C.CONFIG_FILE)
cfg.write(contents)
assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0
assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0
assert cfg.read() == contents
def test_autoupdate_old_revision_broken(tempdir_factory, in_tmpdir):
def test_autoupdate_old_revision_broken(tempdir_factory, in_tmpdir, store):
"""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):
write_config('.', config)
with open(C.CONFIG_FILE) as f:
before = f.read()
assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 0
assert autoupdate(C.CONFIG_FILE, store, 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):
def test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store):
fmt = (
'repos:\n'
'- repo: {}\n'
@ -192,24 +192,24 @@ def test_autoupdate_out_of_date_repo(out_of_date, tmpdir):
cfg = tmpdir.join(C.CONFIG_FILE)
cfg.write(fmt.format(out_of_date.path, out_of_date.original_rev))
assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0
assert autoupdate(str(cfg), store, 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):
def test_autoupdate_with_core_useBuiltinFSMonitor(out_of_date, tmpdir, store):
# 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)
test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store)
def test_autoupdate_pure_yaml(out_of_date, tmpdir):
def test_autoupdate_pure_yaml(out_of_date, tmpdir, store):
with mock.patch.object(yaml, 'Dumper', yaml.yaml.SafeDumper):
test_autoupdate_out_of_date_repo(out_of_date, tmpdir)
test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store)
def test_autoupdate_only_one_to_update(up_to_date, out_of_date, tmpdir):
def test_autoupdate_only_one_to_update(up_to_date, out_of_date, tmpdir, store):
fmt = (
'repos:\n'
'- repo: {}\n'
@ -228,7 +228,7 @@ def test_autoupdate_only_one_to_update(up_to_date, out_of_date, tmpdir):
)
cfg.write(before)
assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0
assert autoupdate(str(cfg), store, 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):
def test_autoupdate_out_of_date_repo_with_correct_repo_name(
out_of_date, in_tmpdir,
out_of_date, in_tmpdir, store,
):
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, freeze=False, tags_only=False,
C.CONFIG_FILE, store, 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,
out_of_date, in_tmpdir, store,
):
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, freeze=False, tags_only=False,
C.CONFIG_FILE, store, 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):
def test_does_not_reformat(tmpdir, out_of_date, store):
fmt = (
'repos:\n'
'- repo: {}\n'
@ -294,12 +294,12 @@ def test_does_not_reformat(tmpdir, out_of_date):
cfg = tmpdir.join(C.CONFIG_FILE)
cfg.write(fmt.format(out_of_date.path, out_of_date.original_rev))
assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0
assert autoupdate(str(cfg), store, 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):
def test_does_not_change_mixed_endlines_read(up_to_date, tmpdir, store):
fmt = (
'repos:\n'
'- repo: {}\n'
@ -314,11 +314,11 @@ def test_does_not_change_mixed_endlines_read(up_to_date, tmpdir):
expected = fmt.format(up_to_date, git.head_rev(up_to_date)).encode()
cfg.write_binary(expected)
assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0
assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0
assert cfg.read_binary() == expected
def test_does_not_change_mixed_endlines_write(tmpdir, out_of_date):
def test_does_not_change_mixed_endlines_write(tmpdir, out_of_date, store):
fmt = (
'repos:\n'
'- repo: {}\n'
@ -333,12 +333,12 @@ def test_does_not_change_mixed_endlines_write(tmpdir, out_of_date):
fmt.format(out_of_date.path, out_of_date.original_rev).encode(),
)
assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0
assert autoupdate(str(cfg), store, 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, tmpdir):
def test_loses_formatting_when_not_detectable(out_of_date, store, 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, tmpdir):
cfg = tmpdir.join(C.CONFIG_FILE)
cfg.write(config)
assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0
assert autoupdate(str(cfg), store, 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, tmpdir):
assert cfg.read() == expected
def test_autoupdate_tagged_repo(tagged, in_tmpdir):
def test_autoupdate_tagged_repo(tagged, in_tmpdir, store):
config = make_config_from_repo(tagged.path, rev=tagged.original_rev)
write_config('.', config)
assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 0
assert autoupdate(C.CONFIG_FILE, store, 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):
def test_autoupdate_freeze(tagged, in_tmpdir, store):
config = make_config_from_repo(tagged.path, rev=tagged.original_rev)
write_config('.', config)
assert autoupdate(C.CONFIG_FILE, freeze=True, tags_only=False) == 0
assert autoupdate(C.CONFIG_FILE, store, 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, freeze=False, tags_only=False) == 0
assert autoupdate(C.CONFIG_FILE, store, 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):
def test_autoupdate_tags_only(tagged, in_tmpdir, store):
# 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, freeze=False, tags_only=True) == 0
assert autoupdate(C.CONFIG_FILE, store, 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):
def test_autoupdate_latest_no_config(out_of_date, in_tmpdir, store):
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):
cmd_output('git', 'rm', '-r', ':/', cwd=out_of_date.path)
git_commit(cwd=out_of_date.path)
assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 1
assert autoupdate(C.CONFIG_FILE, store, 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):
def test_hook_disppearing_repo_raises(hook_disappearing, store):
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):
)
info = RevInfo.from_config(config).update(tags_only=False, freeze=False)
with pytest.raises(RepositoryCannotBeUpdatedError):
_check_hooks_still_exist_at_rev(config, info)
_check_hooks_still_exist_at_rev(config, info, store)
def test_autoupdate_hook_disappearing_repo(hook_disappearing, tmpdir):
def test_autoupdate_hook_disappearing_repo(hook_disappearing, tmpdir, store):
contents = (
f'repos:\n'
f'- repo: {hook_disappearing.path}\n'
@ -442,21 +442,21 @@ def test_autoupdate_hook_disappearing_repo(hook_disappearing, tmpdir):
cfg = tmpdir.join(C.CONFIG_FILE)
cfg.write(contents)
assert autoupdate(str(cfg), freeze=False, tags_only=False) == 1
assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 1
assert cfg.read() == contents
def test_autoupdate_local_hooks(in_git_dir):
def test_autoupdate_local_hooks(in_git_dir, store):
config = sample_local_config()
add_config_to_repo('.', config)
assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 0
assert autoupdate(C.CONFIG_FILE, store, 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,
out_of_date, in_tmpdir, store,
):
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, freeze=False, tags_only=False) == 0
assert autoupdate(C.CONFIG_FILE, store, 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):
def test_autoupdate_meta_hooks(tmpdir, store):
cfg = tmpdir.join(C.CONFIG_FILE)
cfg.write(
'repos:\n'
@ -478,7 +478,7 @@ def test_autoupdate_meta_hooks(tmpdir):
' hooks:\n'
' - id: check-useless-excludes\n',
)
assert autoupdate(str(cfg), freeze=False, tags_only=True) == 0
assert autoupdate(str(cfg), store, freeze=False, tags_only=True) == 0
assert cfg.read() == (
'repos:\n'
'- repo: meta\n'
@ -487,7 +487,7 @@ def test_autoupdate_meta_hooks(tmpdir):
)
def test_updates_old_format_to_new_format(tmpdir, capsys):
def test_updates_old_format_to_new_format(tmpdir, capsys, store):
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):
' entry: ./bin/foo.sh\n'
' language: script\n',
)
assert autoupdate(str(cfg), freeze=False, tags_only=True) == 0
assert autoupdate(str(cfg), store, 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):
assert out == 'Configuration has been migrated.\n'
def test_maintains_rev_quoting_style(tmpdir, out_of_date):
def test_maintains_rev_quoting_style(tmpdir, out_of_date, store):
fmt = (
'repos:\n'
'- repo: {path}\n'
@ -527,6 +527,6 @@ def test_maintains_rev_quoting_style(tmpdir, out_of_date):
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), freeze=False, tags_only=False) == 0
assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0
expected = fmt.format(path=out_of_date.path, rev=out_of_date.head_rev)
assert cfg.read() == expected

View file

@ -19,13 +19,11 @@ from testing.util import git_commit
def _repo_count(store):
with store.connect() as db:
return db.execute('SELECT COUNT(1) FROM repos').fetchone()[0]
return len(store.select_all_repos())
def _config_count(store):
with store.connect() as db:
return db.execute('SELECT COUNT(1) FROM configs').fetchone()[0]
return len(store.select_all_configs())
def _remove_config_assert_cleared(store, cap_out):
@ -45,9 +43,8 @@ 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
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)
install_hooks(C.CONFIG_FILE, store)
assert not autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False)
assert _config_count(store) == 1
assert _repo_count(store) == 2
@ -155,8 +152,7 @@ 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
with store.connect() as db:
path, = db.execute('SELECT path FROM repos').fetchone()
(_, _, path), = store.select_all_repos()
os.remove(os.path.join(path, C.MANIFEST_FILE))
assert _config_count(store) == 1
@ -165,11 +161,3 @@ 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'

View file

@ -1,99 +0,0 @@
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

View file

@ -349,9 +349,8 @@ 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
legacy = 'legacy hook\n'
assert output.startswith(legacy)
NORMAL_PRE_COMMIT_RUN.assert_matches(output.removeprefix(legacy))
assert output.startswith('legacy hook\n')
NORMAL_PRE_COMMIT_RUN.assert_matches(output[len('legacy hook\n'):])
def test_legacy_overwriting_legacy_hook(tempdir_factory, store):
@ -376,9 +375,8 @@ 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
legacy = 'legacy hook\n'
assert output.startswith(legacy)
NORMAL_PRE_COMMIT_RUN.assert_matches(output.removeprefix(legacy))
assert output.startswith('legacy hook\n')
NORMAL_PRE_COMMIT_RUN.assert_matches(output[len('legacy hook\n'):])
def test_install_with_existing_non_utf8_script(tmpdir, store):

View file

@ -1,26 +1,10 @@
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):
@ -150,27 +134,6 @@ 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:
@ -204,73 +167,6 @@ repos:
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)

View file

@ -4,7 +4,7 @@ import os.path
import shlex
import sys
import time
from collections.abc import MutableMapping
from typing 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, 'monotonic', side_effect=(t1, t2)):
with mock.patch.object(time, 'time', side_effect=(t1, t2)):
ret, printed = _do_run(cap_out, store, str(in_git_dir), opts)
assert ret == 0
assert expected in printed
@ -1088,35 +1088,6 @@ 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 == []
@ -1156,8 +1127,8 @@ def test_classifier_empty_types_or(tmpdir):
types_or=[],
exclude_types=[],
)
assert tuple(for_symlink) == ('foo',)
assert tuple(for_file) == ('bar',)
assert for_symlink == ['foo']
assert for_file == ['bar']
@pytest.fixture
@ -1171,33 +1142,33 @@ def some_filenames():
def test_include_exclude_base_case(some_filenames):
ret = filter_by_include_exclude(some_filenames, '', '^$')
assert tuple(ret) == (
assert 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 tuple(ret) == ('link',)
assert ret == ['link']
def test_include_exclude_total_match(some_filenames):
ret = filter_by_include_exclude(some_filenames, r'^.*\.py$', '^$')
assert tuple(ret) == ('pre_commit/git.py', 'pre_commit/main.py')
assert 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 tuple(ret) == ('.pre-commit-hooks.yaml',)
assert ret == ['.pre-commit-hooks.yaml']
def test_include_exclude_exclude_removes_files(some_filenames):
ret = filter_by_include_exclude(some_filenames, '', r'\.py$')
assert tuple(ret) == ('.pre-commit-hooks.yaml',)
assert ret == ['.pre-commit-hooks.yaml']
def test_args_hook_only(cap_out, store, repo_with_passing_hook):

View file

@ -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, 'monotonic', return_value=0.0):
with mock.patch.object(time, 'time', return_value=0.0):
_run_try_repo(tempdir_factory, verbose=True)
start, config, rest = _get_out(cap_out)
assert start == ''

View file

@ -2,6 +2,7 @@ from __future__ import annotations
import functools
import io
import logging
import os.path
from unittest import mock
@ -202,25 +203,42 @@ 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: io.BytesIO) -> None:
def __init__(self, stream):
self._stream = stream
def get_bytes(self) -> bytes:
def get_bytes(self):
"""Get the output as-if no encoding occurred"""
data = self._stream.getvalue()
self._stream.seek(0)
self._stream.truncate()
data = self._stream.data.getvalue()
self._stream.data.seek(0)
self._stream.data.truncate()
return data.replace(b'\r\n', b'\n')
def get(self) -> str:
def get(self):
"""Get the output assuming it was written as UTF-8 bytes"""
return self.get_bytes().decode()
@pytest.fixture
def cap_out():
stream = io.BytesIO()
stream = FakeStream()
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):

View file

@ -141,15 +141,6 @@ 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'

View file

@ -1,5 +1,6 @@
from __future__ import annotations
import multiprocessing
import os.path
import sys
from unittest import mock
@ -9,7 +10,6 @@ import pytest
import pre_commit.constants as C
from pre_commit import lang_base
from pre_commit import parse_shebang
from pre_commit import xargs
from pre_commit.prefix import Prefix
from pre_commit.util import CalledProcessError
@ -116,23 +116,30 @@ def test_no_env_noop(tmp_path):
assert before == inside == after
@pytest.fixture
def cpu_count_mck():
with mock.patch.object(xargs, 'cpu_count', return_value=4):
yield
def test_target_concurrency_normal():
with mock.patch.object(multiprocessing, 'cpu_count', return_value=123):
with mock.patch.dict(os.environ, {}, clear=True):
assert lang_base.target_concurrency() == 123
@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_target_concurrency_testing_env_var():
with mock.patch.dict(
os.environ, {'PRE_COMMIT_NO_CONCURRENCY': '1'}, clear=True,
):
assert lang_base.target_concurrency() == 1
def test_target_concurrency_on_travis():
with mock.patch.dict(os.environ, {'TRAVIS': '1'}, clear=True):
assert lang_base.target_concurrency() == 2
def test_target_concurrency_cpu_count_not_implemented():
with mock.patch.object(
multiprocessing, 'cpu_count', side_effect=NotImplementedError,
):
with mock.patch.dict(os.environ, {}, clear=True):
assert lang_base.target_concurrency() == 1
def test_shuffled_is_deterministic():
@ -164,15 +171,3 @@ def test_basic_run_hook(tmp_path):
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',
)

View file

@ -10,7 +10,7 @@ from testing.language_helpers import run_language
def test_dart(tmp_path):
pubspec_yaml = '''\
environment:
sdk: '>=2.12.0 <4.0.0'
sdk: '>=2.10.0 <3.0.0'
name: hello_world_dart

View file

@ -1,18 +1,10 @@
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(
@ -33,27 +25,3 @@ def test_docker_image_hook_via_args(tmp_path):
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')

View file

@ -14,173 +14,40 @@ from pre_commit.util import CalledProcessError
from testing.language_helpers import run_language
from testing.util import xfailif_windows
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
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
''' # noqa: E501
# The ID should match the above cgroup example.
CONTAINER_ID = 'c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7' # noqa: E501
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
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
'''
def test_docker_fallback_user():
@ -195,46 +62,9 @@ def test_docker_fallback_user():
assert docker.get_docker_user() == ()
@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():
def test_in_docker_no_file():
with mock.patch.object(builtins, 'open', side_effect=FileNotFoundError):
assert docker._get_container_id() is None
assert docker._is_in_docker() is False
def _mock_open(data):
@ -246,33 +76,38 @@ def _mock_open(data):
)
def test_container_id_not_in_file():
with _mock_open(NON_DOCKER_MOUNTINFO_EXAMPLE):
assert docker._get_container_id() is None
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_get_container_id():
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):
with _mock_open(DOCKER_CGROUP_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_open(b''):
with mock.patch.object(docker, '_is_in_docker', return_value=False):
assert docker._get_docker_path('abc') == 'abc'
@pytest.fixture
def in_docker():
with mock.patch.object(
docker, '_get_container_id', return_value=CONTAINER_ID,
):
yield
with mock.patch.object(docker, '_is_in_docker', return_value=True):
with mock.patch.object(
docker, '_get_container_id', return_value=CONTAINER_ID,
):
yield
def _linux_commonpath():
@ -360,14 +195,3 @@ CMD ["echo", "This is overwritten by the entry"']
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

View file

@ -27,7 +27,7 @@ def _csproj(tool_name):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8</TargetFramework>
<TargetFramework>net6</TargetFramework>
<PackAsTool>true</PackAsTool>
<ToolCommandName>{tool_name}</ToolCommandName>
<PackageOutputPath>./nupkg</PackageOutputPath>

View file

@ -7,18 +7,10 @@ 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.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__
@ -119,11 +111,11 @@ def test_golang_versioned(tmp_path):
tmp_path,
golang,
'go version',
version='1.21.1',
version='1.18.4',
)
assert ret == 0
assert out.startswith(b'go version go1.21.1')
assert out.startswith(b'go version go1.18.4')
def test_local_golang_additional_deps(tmp_path):
@ -136,101 +128,9 @@ def test_local_golang_additional_deps(tmp_path):
deps=('golang.org/x/example/hello@latest',),
)
assert ret == (0, b'Hello, world!\n')
assert ret == (0, b'Hello, Go examples!\n')
def test_golang_hook_still_works_when_gobin_is_set(tmp_path):
with envcontext((('GOBIN', str(tmp_path.joinpath('gobin'))),)):
test_golang_system(tmp_path)
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()

View file

@ -1,50 +0,0 @@
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'

View file

@ -1,111 +0,0 @@
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

View file

@ -139,7 +139,7 @@ def test_node_with_user_config_set(tmp_path):
test_node_hook_system(tmp_path)
@pytest.mark.parametrize('version', (C.DEFAULT, '18.14.0'))
@pytest.mark.parametrize('version', (C.DEFAULT, '18.13.0'))
def test_node_hook_versions(tmp_path, version):
_make_hello_world(tmp_path)
ret = run_language(tmp_path, node, 'node-hello', version=version)

View file

@ -10,11 +10,8 @@ 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
@ -37,72 +34,6 @@ 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 sys.platform == 'win32': # pragma: win32 cover
@ -353,15 +284,3 @@ def test_python_hook_weird_setup_cfg(tmp_path):
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 ')

View file

@ -1,17 +1,14 @@
from __future__ import annotations
import os.path
from unittest import mock
import shutil
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
@ -130,8 +127,7 @@ def test_path_rscript_exec_no_r_home_set():
assert r._rscript_exec() == 'Rscript'
@pytest.fixture
def renv_lock_file(tmp_path):
def test_r_hook(tmp_path):
renv_lock = '''\
{
"R": {
@ -161,12 +157,6 @@ def renv_lock_file(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)
@ -188,39 +178,27 @@ RoxygenNote: 7.1.1
Imports:
rprojroot
'''
tmp_path.joinpath('DESCRIPTION').write_text(description)
yield
@pytest.fixture
def hello_world_file(tmp_path):
hello_world = '''\
hello_world_r = '''\
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
@pytest.fixture
def renv_folder(tmp_path):
tmp_path.joinpath('renv.lock').write_text(renv_lock)
tmp_path.joinpath('DESCRIPTION').write_text(description)
tmp_path.joinpath('hello-world.R').write_text(hello_world_r)
renv_dir = tmp_path.joinpath('renv')
renv_dir.mkdir()
activate_r = resource_text('empty_template_activate.R')
renv_dir.joinpath('activate.R').write_text(activate_r)
yield
shutil.copy(
os.path.join(
os.path.dirname(__file__),
'../../pre_commit/resources/empty_template_activate.R',
),
renv_dir.joinpath('activate.R'),
)
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
@ -243,55 +221,3 @@ 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)

View file

@ -91,8 +91,8 @@ def test_ruby_additional_deps(tmp_path):
tmp_path,
ruby,
'ruby -e',
args=('require "jmespath"',),
deps=('jmespath',),
args=('require "tins"',),
deps=('tins',),
)
assert ret == (0, b'')

View file

@ -9,7 +9,6 @@ from pre_commit import parse_shebang
from pre_commit.languages import rust
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,14 +29,6 @@ def test_uses_default_when_rust_is_not_available(cmd_output_b_mck):
assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT
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()

View file

@ -1,14 +1,14 @@
from __future__ import annotations
from pre_commit.languages import unsupported_script
from pre_commit.languages import script
from pre_commit.util import make_executable
from testing.language_helpers import run_language
def test_unsupported_script_language(tmp_path):
def test_script_language(tmp_path):
exe = tmp_path.joinpath('main')
exe.write_text('#!/usr/bin/env bash\necho hello hello world\n')
make_executable(exe)
expected = (0, b'hello hello world\n')
assert run_language(tmp_path, unsupported_script, 'main') == expected
assert run_language(tmp_path, script, 'main') == expected

Some files were not shown because too many files have changed in this diff Show more