1
0
Fork 0

Adding upstream version 3.5.0.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-09 21:45:27 +01:00
parent 0db83eaf7b
commit 3e8db4df26
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
14 changed files with 130 additions and 106 deletions

View file

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0 rev: v4.5.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: end-of-file-fixer - id: end-of-file-fixer
@ -10,25 +10,25 @@ repos:
- id: name-tests-test - id: name-tests-test
- id: requirements-txt-fixer - id: requirements-txt-fixer
- repo: https://github.com/asottile/setup-cfg-fmt - repo: https://github.com/asottile/setup-cfg-fmt
rev: v2.4.0 rev: v2.5.0
hooks: hooks:
- id: setup-cfg-fmt - id: setup-cfg-fmt
- repo: https://github.com/asottile/reorder-python-imports - repo: https://github.com/asottile/reorder-python-imports
rev: v3.10.0 rev: v3.12.0
hooks: hooks:
- id: reorder-python-imports - id: reorder-python-imports
exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/)
args: [--py38-plus, --add-import, 'from __future__ import annotations'] args: [--py38-plus, --add-import, 'from __future__ import annotations']
- repo: https://github.com/asottile/add-trailing-comma - repo: https://github.com/asottile/add-trailing-comma
rev: v3.0.1 rev: v3.1.0
hooks: hooks:
- id: add-trailing-comma - id: add-trailing-comma
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.10.1 rev: v3.15.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py38-plus] args: [--py38-plus]
- repo: https://github.com/pre-commit/mirrors-autopep8 - repo: https://github.com/hhatto/autopep8
rev: v2.0.4 rev: v2.0.4
hooks: hooks:
- id: autopep8 - id: autopep8

View file

@ -1,3 +1,20 @@
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.
### Migrating
- Require npm 6.x+ for `language: node` hooks.
- #2996 PR by @RoelAdriaans.
- #1983 issue by @henryiii.
3.4.0 - 2023-09-02 3.4.0 - 2023-09-02
================== ==================

View file

@ -10,7 +10,8 @@ import subprocess
import time import time
import unicodedata import unicodedata
from typing import Any from typing import Any
from typing import Collection from typing import Generator
from typing import Iterable
from typing import MutableMapping from typing import MutableMapping
from typing import Sequence from typing import Sequence
@ -57,20 +58,20 @@ def _full_msg(
def filter_by_include_exclude( def filter_by_include_exclude(
names: Collection[str], names: Iterable[str],
include: str, include: str,
exclude: str, exclude: str,
) -> list[str]: ) -> Generator[str, None, None]:
include_re, exclude_re = re.compile(include), re.compile(exclude) include_re, exclude_re = re.compile(include), re.compile(exclude)
return [ return (
filename for filename in names filename for filename in names
if include_re.search(filename) if include_re.search(filename)
if not exclude_re.search(filename) if not exclude_re.search(filename)
] )
class Classifier: class Classifier:
def __init__(self, filenames: Collection[str]) -> None: def __init__(self, filenames: Iterable[str]) -> None:
self.filenames = [f for f in filenames if os.path.lexists(f)] self.filenames = [f for f in filenames if os.path.lexists(f)]
@functools.lru_cache(maxsize=None) @functools.lru_cache(maxsize=None)
@ -79,15 +80,14 @@ class Classifier:
def by_types( def by_types(
self, self,
names: Sequence[str], names: Iterable[str],
types: Collection[str], types: Iterable[str],
types_or: Collection[str], types_or: Iterable[str],
exclude_types: Collection[str], exclude_types: Iterable[str],
) -> list[str]: ) -> Generator[str, None, None]:
types = frozenset(types) types = frozenset(types)
types_or = frozenset(types_or) types_or = frozenset(types_or)
exclude_types = frozenset(exclude_types) exclude_types = frozenset(exclude_types)
ret = []
for filename in names: for filename in names:
tags = self._types_for_file(filename) tags = self._types_for_file(filename)
if ( if (
@ -95,24 +95,24 @@ class Classifier:
(not types_or or tags & types_or) and (not types_or or tags & types_or) and
not tags & exclude_types not tags & exclude_types
): ):
ret.append(filename) yield filename
return ret
def filenames_for_hook(self, hook: Hook) -> tuple[str, ...]: def filenames_for_hook(self, hook: Hook) -> Generator[str, None, None]:
names = self.filenames return self.by_types(
names = filter_by_include_exclude(names, hook.files, hook.exclude) filter_by_include_exclude(
names = self.by_types( self.filenames,
names, hook.files,
hook.exclude,
),
hook.types, hook.types,
hook.types_or, hook.types_or,
hook.exclude_types, hook.exclude_types,
) )
return tuple(names)
@classmethod @classmethod
def from_config( def from_config(
cls, cls,
filenames: Collection[str], filenames: Iterable[str],
include: str, include: str,
exclude: str, exclude: str,
) -> Classifier: ) -> Classifier:
@ -121,7 +121,7 @@ class Classifier:
# this also makes improperly quoted shell-based hooks work better # this also makes improperly quoted shell-based hooks work better
# see #1173 # see #1173
if os.altsep == '/' and os.sep == '\\': 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) filenames = filter_by_include_exclude(filenames, include, exclude)
return Classifier(filenames) return Classifier(filenames)
@ -148,7 +148,7 @@ def _run_single_hook(
verbose: bool, verbose: bool,
use_color: bool, use_color: bool,
) -> tuple[bool, bytes]: ) -> tuple[bool, bytes]:
filenames = classifier.filenames_for_hook(hook) filenames = tuple(classifier.filenames_for_hook(hook))
if hook.id in skips or hook.alias in skips: if hook.id in skips or hook.alias in skips:
output.write( output.write(
@ -187,7 +187,7 @@ def _run_single_hook(
if not hook.pass_filenames: if not hook.pass_filenames:
filenames = () filenames = ()
time_before = time.time() time_before = time.monotonic()
language = languages[hook.language] language = languages[hook.language]
with language.in_env(hook.prefix, hook.language_version): with language.in_env(hook.prefix, hook.language_version):
retcode, out = language.run_hook( retcode, out = language.run_hook(
@ -199,7 +199,7 @@ def _run_single_hook(
require_serial=hook.require_serial, require_serial=hook.require_serial,
color=use_color, color=use_color,
) )
duration = round(time.time() - time_before, 2) or 0 duration = round(time.monotonic() - time_before, 2) or 0
diff_after = _get_diff() diff_after = _get_diff()
# if the hook makes changes, fail the commit # if the hook makes changes, fail the commit
@ -250,7 +250,7 @@ def _compute_cols(hooks: Sequence[Hook]) -> int:
return max(cols, 80) return max(cols, 80)
def _all_filenames(args: argparse.Namespace) -> Collection[str]: def _all_filenames(args: argparse.Namespace) -> Iterable[str]:
# these hooks do not operate on files # these hooks do not operate on files
if args.hook_stage in { if args.hook_stage in {
'post-checkout', 'post-commit', 'post-merge', 'post-rewrite', 'post-checkout', 'post-commit', 'post-merge', 'post-rewrite',

View file

@ -93,7 +93,7 @@ def install_environment(
# install as if we installed from git # install as if we installed from git
local_install_cmd = ( local_install_cmd = (
'npm', 'install', '--dev', '--prod', 'npm', 'install', '--include=dev', '--include=prod',
'--ignore-prepublish', '--no-progress', '--no-save', '--ignore-prepublish', '--no-progress', '--no-save',
) )
lang_base.setup_cmd(prefix, local_install_cmd) lang_base.setup_cmd(prefix, local_install_cmd)

View file

@ -21,7 +21,7 @@ def check_all_hooks_match_files(config_file: str) -> int:
for hook in all_hooks(config, Store()): for hook in all_hooks(config, Store()):
if hook.always_run or hook.language == 'fail': if hook.always_run or hook.language == 'fail':
continue continue
elif not classifier.filenames_for_hook(hook): elif not any(classifier.filenames_for_hook(hook)):
print(f'{hook.id} does not apply to this repository') print(f'{hook.id} does not apply to this repository')
retv = 1 retv = 1

View file

@ -2,6 +2,7 @@ from __future__ import annotations
import argparse import argparse
import re import re
from typing import Iterable
from typing import Sequence from typing import Sequence
from cfgv import apply_defaults from cfgv import apply_defaults
@ -14,7 +15,7 @@ from pre_commit.commands.run import Classifier
def exclude_matches_any( def exclude_matches_any(
filenames: Sequence[str], filenames: Iterable[str],
include: str, include: str,
exclude: str, exclude: str,
) -> bool: ) -> bool:
@ -50,11 +51,12 @@ def check_useless_excludes(config_file: str) -> int:
# Not actually a manifest dict, but this more accurately reflects # Not actually a manifest dict, but this more accurately reflects
# the defaults applied during runtime # the defaults applied during runtime
hook = apply_defaults(hook, MANIFEST_HOOK_DICT) hook = apply_defaults(hook, MANIFEST_HOOK_DICT)
names = classifier.filenames names = classifier.by_types(
types = hook['types'] classifier.filenames,
types_or = hook['types_or'] hook['types'],
exclude_types = hook['exclude_types'] hook['types_or'],
names = classifier.by_types(names, types, types_or, exclude_types) hook['exclude_types'],
)
include, exclude = hook['files'], hook['exclude'] include, exclude = hook['files'], hook['exclude']
if not exclude_matches_any(names, include, exclude): if not exclude_matches_any(names, include, exclude):
print( print(

View file

@ -1,6 +1,6 @@
[metadata] [metadata]
name = pre_commit name = pre_commit
version = 3.4.0 version = 3.5.0
description = A framework for managing and maintaining multi-language pre-commit hooks. description = A framework for managing and maintaining multi-language pre-commit hooks.
long_description = file: README.md long_description = file: README.md
long_description_content_type = text/markdown long_description_content_type = text/markdown

View file

@ -293,7 +293,7 @@ def test_verbose_duration(cap_out, store, in_git_dir, t1, t2, expected):
write_config('.', {'repo': 'meta', 'hooks': [{'id': 'identity'}]}) write_config('.', {'repo': 'meta', 'hooks': [{'id': 'identity'}]})
cmd_output('git', 'add', '.') cmd_output('git', 'add', '.')
opts = run_opts(verbose=True) opts = run_opts(verbose=True)
with mock.patch.object(time, 'time', side_effect=(t1, t2)): with mock.patch.object(time, 'monotonic', side_effect=(t1, t2)):
ret, printed = _do_run(cap_out, store, str(in_git_dir), opts) ret, printed = _do_run(cap_out, store, str(in_git_dir), opts)
assert ret == 0 assert ret == 0
assert expected in printed assert expected in printed
@ -1127,8 +1127,8 @@ def test_classifier_empty_types_or(tmpdir):
types_or=[], types_or=[],
exclude_types=[], exclude_types=[],
) )
assert for_symlink == ['foo'] assert tuple(for_symlink) == ('foo',)
assert for_file == ['bar'] assert tuple(for_file) == ('bar',)
@pytest.fixture @pytest.fixture
@ -1142,33 +1142,33 @@ def some_filenames():
def test_include_exclude_base_case(some_filenames): def test_include_exclude_base_case(some_filenames):
ret = filter_by_include_exclude(some_filenames, '', '^$') ret = filter_by_include_exclude(some_filenames, '', '^$')
assert ret == [ assert tuple(ret) == (
'.pre-commit-hooks.yaml', '.pre-commit-hooks.yaml',
'pre_commit/git.py', 'pre_commit/git.py',
'pre_commit/main.py', 'pre_commit/main.py',
] )
def test_matches_broken_symlink(tmpdir): def test_matches_broken_symlink(tmpdir):
with tmpdir.as_cwd(): with tmpdir.as_cwd():
os.symlink('does-not-exist', 'link') os.symlink('does-not-exist', 'link')
ret = filter_by_include_exclude({'link'}, '', '^$') ret = filter_by_include_exclude({'link'}, '', '^$')
assert ret == ['link'] assert tuple(ret) == ('link',)
def test_include_exclude_total_match(some_filenames): def test_include_exclude_total_match(some_filenames):
ret = filter_by_include_exclude(some_filenames, r'^.*\.py$', '^$') ret = filter_by_include_exclude(some_filenames, r'^.*\.py$', '^$')
assert ret == ['pre_commit/git.py', 'pre_commit/main.py'] assert tuple(ret) == ('pre_commit/git.py', 'pre_commit/main.py')
def test_include_exclude_does_search_instead_of_match(some_filenames): def test_include_exclude_does_search_instead_of_match(some_filenames):
ret = filter_by_include_exclude(some_filenames, r'\.yaml$', '^$') ret = filter_by_include_exclude(some_filenames, r'\.yaml$', '^$')
assert ret == ['.pre-commit-hooks.yaml'] assert tuple(ret) == ('.pre-commit-hooks.yaml',)
def test_include_exclude_exclude_removes_files(some_filenames): def test_include_exclude_exclude_removes_files(some_filenames):
ret = filter_by_include_exclude(some_filenames, '', r'\.py$') ret = filter_by_include_exclude(some_filenames, '', r'\.py$')
assert ret == ['.pre-commit-hooks.yaml'] assert tuple(ret) == ('.pre-commit-hooks.yaml',)
def test_args_hook_only(cap_out, store, repo_with_passing_hook): 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): def test_try_repo_repo_only(cap_out, tempdir_factory):
with mock.patch.object(time, 'time', return_value=0.0): with mock.patch.object(time, 'monotonic', return_value=0.0):
_run_try_repo(tempdir_factory, verbose=True) _run_try_repo(tempdir_factory, verbose=True)
start, config, rest = _get_out(cap_out) start, config, rest = _get_out(cap_out)
assert start == '' assert start == ''

View file

@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
import multiprocessing
import os.path import os.path
import sys import sys
from unittest import mock from unittest import mock
@ -10,6 +9,7 @@ import pytest
import pre_commit.constants as C import pre_commit.constants as C
from pre_commit import lang_base from pre_commit import lang_base
from pre_commit import parse_shebang from pre_commit import parse_shebang
from pre_commit import xargs
from pre_commit.prefix import Prefix from pre_commit.prefix import Prefix
from pre_commit.util import CalledProcessError from pre_commit.util import CalledProcessError
@ -30,19 +30,6 @@ def homedir_mck():
yield yield
@pytest.fixture
def no_sched_getaffinity():
# Simulates an OS without os.sched_getaffinity available (mac/windows)
# https://docs.python.org/3/library/os.html#interface-to-the-scheduler
with mock.patch.object(
os,
'sched_getaffinity',
create=True,
side_effect=AttributeError,
):
yield
def test_exe_exists_does_not_exist(find_exe_mck, homedir_mck): def test_exe_exists_does_not_exist(find_exe_mck, homedir_mck):
find_exe_mck.return_value = None find_exe_mck.return_value = None
assert lang_base.exe_exists('ruby') is False assert lang_base.exe_exists('ruby') is False
@ -129,40 +116,23 @@ def test_no_env_noop(tmp_path):
assert before == inside == after assert before == inside == after
def test_target_concurrency_sched_getaffinity(no_sched_getaffinity): @pytest.fixture
with mock.patch.object( def cpu_count_mck():
os, with mock.patch.object(xargs, 'cpu_count', return_value=4):
'sched_getaffinity', yield
return_value=set(range(345)),
):
with mock.patch.dict(os.environ, clear=True):
assert lang_base.target_concurrency() == 345
def test_target_concurrency_without_sched_getaffinity(no_sched_getaffinity): @pytest.mark.parametrize(
with mock.patch.object(multiprocessing, 'cpu_count', return_value=123): ('var', 'expected'),
with mock.patch.dict(os.environ, {}, clear=True): (
assert lang_base.target_concurrency() == 123 ('PRE_COMMIT_NO_CONCURRENCY', 1),
('TRAVIS', 2),
(None, 4),
def test_target_concurrency_testing_env_var(): ),
with mock.patch.dict( )
os.environ, {'PRE_COMMIT_NO_CONCURRENCY': '1'}, clear=True, 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() == 1 assert lang_base.target_concurrency() == expected
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(no_sched_getaffinity):
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(): def test_shuffled_is_deterministic():

View file

@ -111,11 +111,11 @@ def test_golang_versioned(tmp_path):
tmp_path, tmp_path,
golang, golang,
'go version', 'go version',
version='1.18.4', version='1.21.1',
) )
assert ret == 0 assert ret == 0
assert out.startswith(b'go version go1.18.4') assert out.startswith(b'go version go1.21.1')
def test_local_golang_additional_deps(tmp_path): def test_local_golang_additional_deps(tmp_path):

View file

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

View file

@ -133,17 +133,17 @@ def test_normalize_cmd_PATH():
def test_normalize_cmd_shebang(in_tmpdir): def test_normalize_cmd_shebang(in_tmpdir):
echo = _echo_exe().replace(os.sep, '/') us = sys.executable.replace(os.sep, '/')
path = write_executable(echo) path = write_executable(us)
assert parse_shebang.normalize_cmd((path,)) == (echo, path) assert parse_shebang.normalize_cmd((path,)) == (us, path)
def test_normalize_cmd_PATH_shebang_full_path(in_tmpdir): def test_normalize_cmd_PATH_shebang_full_path(in_tmpdir):
echo = _echo_exe().replace(os.sep, '/') us = sys.executable.replace(os.sep, '/')
path = write_executable(echo) path = write_executable(us)
with bin_on_path(): with bin_on_path():
ret = parse_shebang.normalize_cmd(('run',)) ret = parse_shebang.normalize_cmd(('run',))
assert ret == (echo, os.path.abspath(path)) assert ret == (us, os.path.abspath(path))
def test_normalize_cmd_PATH_shebang_PATH(in_tmpdir): def test_normalize_cmd_PATH_shebang_PATH(in_tmpdir):

View file

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import concurrent.futures import concurrent.futures
import multiprocessing
import os import os
import sys import sys
import time import time
@ -12,6 +13,40 @@ from pre_commit import parse_shebang
from pre_commit import xargs from pre_commit import xargs
def test_cpu_count_sched_getaffinity_exists():
with mock.patch.object(
os, 'sched_getaffinity', create=True, return_value=set(range(345)),
):
assert xargs.cpu_count() == 345
@pytest.fixture
def no_sched_getaffinity():
# Simulates an OS without os.sched_getaffinity available (mac/windows)
# https://docs.python.org/3/library/os.html#interface-to-the-scheduler
with mock.patch.object(
os,
'sched_getaffinity',
create=True,
side_effect=AttributeError,
):
yield
def test_cpu_count_multiprocessing_cpu_count_implemented(no_sched_getaffinity):
with mock.patch.object(multiprocessing, 'cpu_count', return_value=123):
assert xargs.cpu_count() == 123
def test_cpu_count_multiprocessing_cpu_count_not_implemented(
no_sched_getaffinity,
):
with mock.patch.object(
multiprocessing, 'cpu_count', side_effect=NotImplementedError,
):
assert xargs.cpu_count() == 1
@pytest.mark.parametrize( @pytest.mark.parametrize(
('env', 'expected'), ('env', 'expected'),
( (