1
0
Fork 0

Adding upstream version 2.5.1.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-09 21:20:03 +01:00
parent 080d7f9289
commit bb6dbf8636
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
37 changed files with 457 additions and 213 deletions

View file

@ -84,7 +84,9 @@ def _check_hooks_still_exist_at_rev(
)
REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([^\s#]+)(.*)(\r?\n)$', re.DOTALL)
REV_LINE_RE = re.compile(
r'^(\s+)rev:(\s*)([\'"]?)([^\s#]+)(.*)(\r?\n)$', re.DOTALL,
)
def _original_lines(
@ -116,15 +118,15 @@ def _write_new_config(path: str, rev_infos: List[Optional[RevInfo]]) -> None:
continue
match = REV_LINE_RE.match(lines[idx])
assert match is not None
new_rev_s = yaml_dump({'rev': rev_info.rev})
new_rev_s = yaml_dump({'rev': rev_info.rev}, default_style=match[3])
new_rev = new_rev_s.split(':', 1)[1].strip()
if rev_info.frozen is not None:
comment = f' # frozen: {rev_info.frozen}'
elif match[4].strip().startswith('# frozen:'):
elif match[5].strip().startswith('# frozen:'):
comment = ''
else:
comment = match[4]
lines[idx] = f'{match[1]}rev:{match[2]}{new_rev}{comment}{match[5]}'
comment = match[5]
lines[idx] = f'{match[1]}rev:{match[2]}{new_rev}{comment}{match[6]}'
with open(path, 'w', newline='') as f:
f.write(''.join(lines))

View file

@ -150,6 +150,7 @@ def _pre_push_ns(
_EXPECTED_ARG_LENGTH_BY_HOOK = {
'commit-msg': 1,
'post-checkout': 3,
'post-commit': 0,
'pre-commit': 0,
'pre-merge-commit': 0,
'pre-push': 2,
@ -186,7 +187,7 @@ def _run_ns(
return _pre_push_ns(color, args, stdin)
elif hook_type in {'commit-msg', 'prepare-commit-msg'}:
return _ns(hook_type, color, commit_msg_filename=args[0])
elif hook_type in {'pre-merge-commit', 'pre-commit'}:
elif hook_type in {'post-commit', 'pre-merge-commit', 'pre-commit'}:
return _ns(hook_type, color)
elif hook_type == 'post-checkout':
return _ns(

View file

@ -2,6 +2,7 @@ import re
import yaml
from pre_commit.clientlib import load_config
from pre_commit.util import yaml_load
@ -43,6 +44,9 @@ def _migrate_sha_to_rev(contents: str) -> str:
def migrate_config(config_file: str, quiet: bool = False) -> int:
# ensure that the configuration is a valid pre-commit configuration
load_config(config_file)
with open(config_file) as f:
orig_contents = contents = f.read()

View file

@ -72,13 +72,7 @@ def filter_by_include_exclude(
class Classifier:
def __init__(self, filenames: Sequence[str]) -> None:
# on windows we normalize all filenames to use forward slashes
# this makes it easier to filter using the `files:` regex
# 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]
def __init__(self, filenames: Collection[str]) -> None:
self.filenames = [f for f in filenames if os.path.lexists(f)]
@functools.lru_cache(maxsize=None)
@ -105,6 +99,22 @@ class Classifier:
names = self.by_types(names, hook.types, hook.exclude_types)
return tuple(names)
@classmethod
def from_config(
cls,
filenames: Collection[str],
include: str,
exclude: str,
) -> 'Classifier':
# on windows we normalize all filenames to use forward slashes
# this makes it easier to filter using the `files:` regex
# 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 = filter_by_include_exclude(filenames, include, exclude)
return Classifier(filenames)
def _get_skips(environ: EnvironT) -> Set[str]:
skips = environ.get('SKIP', '')
@ -221,7 +231,8 @@ def _compute_cols(hooks: Sequence[Hook]) -> int:
def _all_filenames(args: argparse.Namespace) -> Collection[str]:
if args.hook_stage == 'post-checkout': # no files for post-checkout
# these hooks do not operate on files
if args.hook_stage in {'post-checkout', 'post-commit'}:
return ()
elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}:
return (args.commit_msg_filename,)
@ -246,10 +257,9 @@ def _run_hooks(
"""Actually run the hooks."""
skips = _get_skips(environ)
cols = _compute_cols(hooks)
filenames = filter_by_include_exclude(
classifier = Classifier.from_config(
_all_filenames(args), config['files'], config['exclude'],
)
classifier = Classifier(filenames)
retval = 0
for hook in hooks:
retval |= _run_single_hook(
@ -323,6 +333,12 @@ def run(
f'`--hook-stage {args.hook_stage}`',
)
return 1
# prevent recursive post-checkout hooks (#1418)
if (
args.hook_stage == 'post-checkout' and
environ.get('_PRE_COMMIT_SKIP_POST_CHECKOUT')
):
return 0
# Expose from-ref / to-ref as environment variables for hooks to consume
if args.from_ref and args.to_ref:
@ -340,6 +356,9 @@ def run(
if args.checkout_type:
environ['PRE_COMMIT_CHECKOUT_TYPE'] = args.checkout_type
# Set pre_commit flag
environ['PRE_COMMIT'] = '1'
with contextlib.ExitStack() as exit_stack:
if stash:
exit_stack.enter_context(staged_files_only(store.directory))

View file

@ -17,8 +17,8 @@ VERSION = importlib_metadata.version('pre_commit')
# `manual` is not invoked by any installed git hook. See #719
STAGES = (
'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', 'manual',
'post-checkout', 'push',
'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg',
'post-commit', 'manual', 'post-checkout', 'push',
)
DEFAULT = 'default'

View file

@ -158,7 +158,8 @@ def init_repo(path: str, remote: str) -> None:
remote = os.path.abspath(remote)
env = no_git_env()
cmd_output_b('git', 'init', path, env=env)
# avoid the user's template so that hooks do not recurse
cmd_output_b('git', 'init', '--template=', path, env=env)
cmd_output_b('git', 'remote', 'add', 'origin', remote, cwd=path, env=env)

View file

@ -14,7 +14,6 @@ from pre_commit.languages import node
from pre_commit.languages import perl
from pre_commit.languages import pygrep
from pre_commit.languages import python
from pre_commit.languages import python_venv
from pre_commit.languages import ruby
from pre_commit.languages import rust
from pre_commit.languages import script
@ -49,7 +48,6 @@ languages = {
'perl': Language(name='perl', ENVIRONMENT_DIR=perl.ENVIRONMENT_DIR, get_default_version=perl.get_default_version, healthy=perl.healthy, install_environment=perl.install_environment, run_hook=perl.run_hook), # noqa: E501
'pygrep': Language(name='pygrep', ENVIRONMENT_DIR=pygrep.ENVIRONMENT_DIR, get_default_version=pygrep.get_default_version, healthy=pygrep.healthy, install_environment=pygrep.install_environment, run_hook=pygrep.run_hook), # noqa: E501
'python': Language(name='python', ENVIRONMENT_DIR=python.ENVIRONMENT_DIR, get_default_version=python.get_default_version, healthy=python.healthy, install_environment=python.install_environment, run_hook=python.run_hook), # noqa: E501
'python_venv': Language(name='python_venv', ENVIRONMENT_DIR=python_venv.ENVIRONMENT_DIR, get_default_version=python_venv.get_default_version, healthy=python_venv.healthy, install_environment=python_venv.install_environment, run_hook=python_venv.run_hook), # noqa: E501
'ruby': Language(name='ruby', ENVIRONMENT_DIR=ruby.ENVIRONMENT_DIR, get_default_version=ruby.get_default_version, healthy=ruby.healthy, install_environment=ruby.install_environment, run_hook=ruby.run_hook), # noqa: E501
'rust': Language(name='rust', ENVIRONMENT_DIR=rust.ENVIRONMENT_DIR, get_default_version=rust.get_default_version, healthy=rust.healthy, install_environment=rust.install_environment, run_hook=rust.run_hook), # noqa: E501
'script': Language(name='script', ENVIRONMENT_DIR=script.ENVIRONMENT_DIR, get_default_version=script.get_default_version, healthy=script.healthy, install_environment=script.install_environment, run_hook=script.run_hook), # noqa: E501
@ -57,4 +55,6 @@ languages = {
'system': Language(name='system', ENVIRONMENT_DIR=system.ENVIRONMENT_DIR, get_default_version=system.get_default_version, healthy=system.healthy, install_environment=system.install_environment, run_hook=system.run_hook), # noqa: E501
# END GENERATED
}
# TODO: fully deprecate `python_venv`
languages['python_venv'] = languages['python']
all_languages = sorted(languages)

View file

@ -18,7 +18,7 @@ from pre_commit.xargs import xargs
if TYPE_CHECKING:
from typing import NoReturn
FIXED_RANDOM_SEED = 1542676186
FIXED_RANDOM_SEED = 1542676187
def run_setup_cmd(prefix: Prefix, cmd: Tuple[str, ...]) -> None:
@ -92,7 +92,7 @@ def _shuffled(seq: Sequence[str]) -> List[str]:
fixed_random.seed(FIXED_RANDOM_SEED, version=1)
seq = list(seq)
random.shuffle(seq, random=fixed_random.random)
fixed_random.shuffle(seq)
return seq

View file

@ -79,7 +79,7 @@ def install_environment(
# https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx?f=255&MSPPError=-2147217396#maxpath
if sys.platform == 'win32': # pragma: no cover
envdir = f'\\\\?\\{os.path.normpath(envdir)}'
envdir = fr'\\?\{os.path.normpath(envdir)}'
with clean_path_on_failure(envdir):
cmd = [
sys.executable, '-mnodeenv', '--prebuilt', '--clean-src', envdir,

View file

@ -2,8 +2,7 @@ import contextlib
import functools
import os
import sys
from typing import Callable
from typing import ContextManager
from typing import Dict
from typing import Generator
from typing import Optional
from typing import Sequence
@ -26,6 +25,28 @@ from pre_commit.util import cmd_output_b
ENVIRONMENT_DIR = 'py_env'
@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:
return cmd_output(exe, '-S', '-c', prog)[1].strip()
except CalledProcessError:
return f'<<error retrieving version from {exe}>>'
def _read_pyvenv_cfg(filename: str) -> Dict[str, str]:
ret = {}
with open(filename) as f:
for line in f:
try:
k, v = line.split('=')
except ValueError: # blank line / comment / etc.
continue
else:
ret[k.strip()] = v.strip()
return ret
def bin_dir(venv: str) -> str:
"""On windows there's a different directory for the virtualenv"""
bin_part = 'Scripts' if os.name == 'nt' else 'bin'
@ -34,6 +55,7 @@ def bin_dir(venv: str) -> str:
def get_env_patch(venv: str) -> PatchesT:
return (
('PIP_DISABLE_PIP_VERSION_CHECK', '1'),
('PYTHONHOME', UNSET),
('VIRTUAL_ENV', venv),
('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))),
@ -45,9 +67,10 @@ def _find_by_py_launcher(
) -> Optional[str]: # pragma: no cover (windows only)
if version.startswith('python'):
num = version[len('python'):]
cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)')
env = dict(os.environ, PYTHONIOENCODING='UTF-8')
try:
cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)')
return cmd_output(*cmd)[1].strip()
return cmd_output(*cmd, env=env)[1].strip()
except CalledProcessError:
pass
return None
@ -115,6 +138,9 @@ def _sys_executable_matches(version: str) -> bool:
def norm_version(version: str) -> str:
if version == C.DEFAULT:
return os.path.realpath(sys.executable)
# first see if our current executable is appropriate
if _sys_executable_matches(version):
return sys.executable
@ -139,70 +165,59 @@ def norm_version(version: str) -> str:
return os.path.expanduser(version)
def py_interface(
_dir: str,
_make_venv: Callable[[str, str], None],
) -> Tuple[
Callable[[Prefix, str], ContextManager[None]],
Callable[[Prefix, str], bool],
Callable[[Hook, Sequence[str], bool], Tuple[int, bytes]],
Callable[[Prefix, str, Sequence[str]], None],
]:
@contextlib.contextmanager
def in_env(
prefix: Prefix,
language_version: str,
) -> Generator[None, None, None]:
envdir = prefix.path(helpers.environment_dir(_dir, language_version))
with envcontext(get_env_patch(envdir)):
yield
def healthy(prefix: Prefix, language_version: str) -> bool:
envdir = helpers.environment_dir(_dir, language_version)
exe_name = 'python.exe' if sys.platform == 'win32' else 'python'
py_exe = prefix.path(bin_dir(envdir), exe_name)
with in_env(prefix, language_version):
retcode, _, _ = cmd_output_b(
py_exe, '-c', 'import ctypes, datetime, io, os, ssl, weakref',
cwd='/',
retcode=None,
)
return retcode == 0
def run_hook(
hook: Hook,
file_args: Sequence[str],
color: bool,
) -> Tuple[int, bytes]:
with in_env(hook.prefix, hook.language_version):
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
def install_environment(
prefix: Prefix,
version: str,
additional_dependencies: Sequence[str],
) -> None:
directory = helpers.environment_dir(_dir, version)
install = ('python', '-mpip', 'install', '.', *additional_dependencies)
env_dir = prefix.path(directory)
with clean_path_on_failure(env_dir):
if version != C.DEFAULT:
python = norm_version(version)
else:
python = os.path.realpath(sys.executable)
_make_venv(env_dir, python)
with in_env(prefix, version):
helpers.run_setup_cmd(prefix, install)
return in_env, healthy, run_hook, install_environment
@contextlib.contextmanager
def in_env(
prefix: Prefix,
language_version: str,
) -> Generator[None, None, None]:
directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version)
envdir = prefix.path(directory)
with envcontext(get_env_patch(envdir)):
yield
def make_venv(envdir: str, python: str) -> None:
env = dict(os.environ, VIRTUALENV_NO_DOWNLOAD='1')
cmd = (sys.executable, '-mvirtualenv', envdir, '-p', python)
cmd_output_b(*cmd, env=env, cwd='/')
def healthy(prefix: Prefix, language_version: str) -> bool:
directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version)
envdir = prefix.path(directory)
pyvenv_cfg = os.path.join(envdir, 'pyvenv.cfg')
# created with "old" virtualenv
if not os.path.exists(pyvenv_cfg):
return False
exe_name = 'python.exe' if sys.platform == 'win32' else 'python'
py_exe = prefix.path(bin_dir(envdir), exe_name)
cfg = _read_pyvenv_cfg(pyvenv_cfg)
return (
'version_info' in cfg and
_version_info(py_exe) == cfg['version_info'] and (
'base-executable' not in cfg or
_version_info(cfg['base-executable']) == cfg['version_info']
)
)
_interface = py_interface(ENVIRONMENT_DIR, make_venv)
in_env, healthy, run_hook, install_environment = _interface
def install_environment(
prefix: Prefix,
version: str,
additional_dependencies: Sequence[str],
) -> None:
envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version))
python = norm_version(version)
venv_cmd = (sys.executable, '-mvirtualenv', envdir, '-p', python)
install_cmd = ('python', '-mpip', 'install', '.', *additional_dependencies)
with clean_path_on_failure(envdir):
cmd_output_b(*venv_cmd, cwd='/')
with in_env(prefix, version):
helpers.run_setup_cmd(prefix, install_cmd)
def run_hook(
hook: Hook,
file_args: Sequence[str],
color: bool,
) -> Tuple[int, bytes]:
with in_env(hook.prefix, hook.language_version):
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)

View file

@ -1,46 +0,0 @@
import os.path
from pre_commit.languages import python
from pre_commit.util import CalledProcessError
from pre_commit.util import cmd_output
from pre_commit.util import cmd_output_b
ENVIRONMENT_DIR = 'py_venv'
get_default_version = python.get_default_version
def orig_py_exe(exe: str) -> str: # pragma: no cover (platform specific)
"""A -mvenv virtualenv made from a -mvirtualenv virtualenv installs
packages to the incorrect location. Attempt to find the _original_ exe
and invoke `-mvenv` from there.
See:
- https://github.com/pre-commit/pre-commit/issues/755
- https://github.com/pypa/virtualenv/issues/1095
- https://bugs.python.org/issue30811
"""
try:
prefix_script = 'import sys; print(sys.real_prefix)'
_, prefix, _ = cmd_output(exe, '-c', prefix_script)
prefix = prefix.strip()
except CalledProcessError:
# not created from -mvirtualenv
return exe
if os.name == 'nt':
expected = os.path.join(prefix, 'python.exe')
else:
expected = os.path.join(prefix, 'bin', os.path.basename(exe))
if os.path.exists(expected):
return expected
else:
return exe
def make_venv(envdir: str, python: str) -> None:
cmd_output_b(orig_py_exe(python), '-mvenv', envdir, cwd='/')
_interface = python.py_interface(ENVIRONMENT_DIR, make_venv)
in_env, healthy, run_hook, install_environment = _interface

View file

@ -9,6 +9,7 @@ from typing import Tuple
import pre_commit.constants as C
from pre_commit.envcontext import envcontext
from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import UNSET
from pre_commit.envcontext import Var
from pre_commit.hook import Hook
from pre_commit.languages import helpers
@ -28,6 +29,7 @@ def get_env_patch(
) -> PatchesT: # pragma: win32 no cover
patches: PatchesT = (
('GEM_HOME', os.path.join(venv, 'gems')),
('GEM_PATH', UNSET),
('RBENV_ROOT', venv),
('BUNDLE_IGNORE_CONFIG', '1'),
(

View file

@ -79,7 +79,7 @@ def _add_hook_type_option(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
'-t', '--hook-type', choices=(
'pre-commit', 'pre-merge-commit', 'pre-push',
'prepare-commit-msg', 'commit-msg', 'post-checkout',
'prepare-commit-msg', 'commit-msg', 'post-commit', 'post-checkout',
),
action=AppendReplaceDefault,
default=['pre-commit'],

View file

@ -11,10 +11,13 @@ from pre_commit.store import Store
def check_all_hooks_match_files(config_file: str) -> int:
classifier = Classifier(git.get_all_files())
config = load_config(config_file)
classifier = Classifier.from_config(
git.get_all_files(), config['files'], config['exclude'],
)
retv = 0
for hook in all_hooks(load_config(config_file), Store()):
for hook in all_hooks(config, Store()):
if hook.always_run or hook.language == 'fail':
continue
elif not classifier.filenames_for_hook(hook):

View file

@ -28,11 +28,14 @@ def exclude_matches_any(
def check_useless_excludes(config_file: str) -> int:
config = load_config(config_file)
classifier = Classifier(git.get_all_files())
filenames = git.get_all_files()
classifier = Classifier.from_config(
filenames, config['files'], config['exclude'],
)
retv = 0
exclude = config['exclude']
if not exclude_matches_any(classifier.filenames, '', exclude):
if not exclude_matches_any(filenames, '', exclude):
print(
f'The global exclude pattern {exclude!r} does not match any files',
)

View file

@ -56,8 +56,10 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]:
with open(patch_filename, 'wb') as patch_file:
patch_file.write(diff_stdout_binary)
# Clear the working directory of unstaged changes
cmd_output_b('git', 'checkout', '--', '.')
# prevent recursive post-checkout hooks (#1418)
no_checkout_env = dict(os.environ, _PRE_COMMIT_SKIP_POST_CHECKOUT='1')
cmd_output_b('git', 'checkout', '--', '.', env=no_checkout_env)
try:
yield
finally:
@ -72,8 +74,9 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]:
# We failed to apply the patch, presumably due to fixes made
# by hooks.
# Roll back the changes made by hooks.
cmd_output_b('git', 'checkout', '--', '.')
cmd_output_b('git', 'checkout', '--', '.', env=no_checkout_env)
_git_apply(patch_filename)
logger.info(f'Restored changes from {patch_filename}.')
else:
# There weren't any staged files so we don't need to do anything

View file

@ -30,10 +30,11 @@ def _get_default_directory() -> str:
`Store.get_default_directory` can be mocked in tests and
`_get_default_directory` can be tested.
"""
return os.environ.get('PRE_COMMIT_HOME') or os.path.join(
ret = os.environ.get('PRE_COMMIT_HOME') or os.path.join(
os.environ.get('XDG_CACHE_HOME') or os.path.expanduser('~/.cache'),
'pre-commit',
)
return os.path.realpath(ret)
class Store:
@ -182,9 +183,9 @@ class Store:
return self._new_repo(repo, ref, deps, clone_strategy)
LOCAL_RESOURCES = (
'Cargo.toml', 'main.go', 'main.rs', '.npmignore', 'package.json',
'pre_commit_dummy_package.gemspec', 'setup.py', 'environment.yml',
'Makefile.PL',
'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore',
'package.json', 'pre_commit_dummy_package.gemspec', 'setup.py',
'environment.yml', 'Makefile.PL',
)
def make_local(self, deps: Sequence[str]) -> str:

View file

@ -36,10 +36,11 @@ yaml_load = functools.partial(yaml.load, Loader=Loader)
Dumper = getattr(yaml, 'CSafeDumper', yaml.SafeDumper)
def yaml_dump(o: Any) -> str:
def yaml_dump(o: Any, **kwargs: Any) -> str:
# when python/mypy#1484 is solved, this can be `functools.partial`
return yaml.dump(
o, Dumper=Dumper, default_flow_style=False, indent=4, sort_keys=False,
**kwargs,
)