1
0
Fork 0

Merging upstream version 2.3.0.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-09 21:19:27 +01:00
parent c869774b2b
commit ac2b33dbed
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
17 changed files with 261 additions and 37 deletions

View file

@ -17,34 +17,34 @@ repos:
- id: flake8 - id: flake8
additional_dependencies: [flake8-typing-imports==1.6.0] additional_dependencies: [flake8-typing-imports==1.6.0]
- repo: https://github.com/pre-commit/mirrors-autopep8 - repo: https://github.com/pre-commit/mirrors-autopep8
rev: v1.5 rev: v1.5.1
hooks: hooks:
- id: autopep8 - id: autopep8
- repo: https://github.com/pre-commit/pre-commit - repo: https://github.com/pre-commit/pre-commit
rev: v2.1.1 rev: v2.2.0
hooks: hooks:
- id: validate_manifest - id: validate_manifest
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v2.0.1 rev: v2.1.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py36-plus] args: [--py36-plus]
- repo: https://github.com/asottile/reorder_python_imports - repo: https://github.com/asottile/reorder_python_imports
rev: v1.9.0 rev: v2.1.0
hooks: hooks:
- id: reorder-python-imports - id: reorder-python-imports
args: [--py3-plus] args: [--py3-plus]
- repo: https://github.com/asottile/add-trailing-comma - repo: https://github.com/asottile/add-trailing-comma
rev: v1.5.0 rev: v2.0.1
hooks: hooks:
- id: add-trailing-comma - id: add-trailing-comma
args: [--py36-plus] args: [--py36-plus]
- repo: https://github.com/asottile/setup-cfg-fmt - repo: https://github.com/asottile/setup-cfg-fmt
rev: v1.6.0 rev: v1.8.2
hooks: hooks:
- id: setup-cfg-fmt - id: setup-cfg-fmt
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.761 rev: v0.770
hooks: hooks:
- id: mypy - id: mypy
exclude: ^testing/resources/ exclude: ^testing/resources/

View file

@ -1,9 +1,34 @@
2.3.0 - 2020-04-22
==================
### Features
- Calculate character width using `east_asian_width`
- #1378 PR by @sophgn.
- Use `language_version: system` by default for `node` hooks if `node` / `npm`
are globally installed.
- #1388 PR by @asottile.
### Fixes
- No longer use a hard-coded user id for docker hooks on windows
- #1371 PR by @killuazhu.
- Fix colors on windows during `git commit`
- #1381 issue by @Cielquan.
- #1382 PR by @asottile.
- Produce readable error message for incorrect argument count to `hook-impl`
- #1394 issue by @pip9ball.
- #1395 PR by @asottile.
- Fix installations which involve an upgrade of `pip` on windows
- #1398 issue by @xiaohuazi123.
- #1399 PR by @asottile.
- Preserve line endings in `pre-commit autoupdate`
- #1402 PR by @utek.
2.2.0 - 2020-03-12 2.2.0 - 2020-03-12
================== ==================
### Features ### Features
- Add support for the `post-checkout` hook - Add support for the `post-checkout` hook
- #1210 issue by @domenkozar. - #1120 issue by @domenkozar.
- #1339 PR by @andrewhare. - #1339 PR by @andrewhare.
- Add more readable `--from-ref` / `--to-ref` aliases for `--source` / - Add more readable `--from-ref` / `--to-ref` aliases for `--source` /
`--origin` `--origin`

View file

@ -11,7 +11,7 @@ if sys.platform == 'win32': # pragma: no cover (windows)
from ctypes.wintypes import DWORD from ctypes.wintypes import DWORD
from ctypes.wintypes import HANDLE from ctypes.wintypes import HANDLE
STD_OUTPUT_HANDLE = -11 STD_ERROR_HANDLE = -12
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
def bool_errcheck(result, func, args): def bool_errcheck(result, func, args):
@ -40,9 +40,9 @@ if sys.platform == 'win32': # pragma: no cover (windows)
# #
# More info on the escape sequences supported: # More info on the escape sequences supported:
# https://msdn.microsoft.com/en-us/library/windows/desktop/mt638032(v=vs.85).aspx # https://msdn.microsoft.com/en-us/library/windows/desktop/mt638032(v=vs.85).aspx
stdout = GetStdHandle(STD_OUTPUT_HANDLE) stderr = GetStdHandle(STD_ERROR_HANDLE)
flags = GetConsoleMode(stdout) flags = GetConsoleMode(stderr)
SetConsoleMode(stdout, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING) SetConsoleMode(stderr, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
try: try:
_enable() _enable()
@ -90,7 +90,7 @@ def use_color(setting: str) -> bool:
return ( return (
setting == 'always' or ( setting == 'always' or (
setting == 'auto' and setting == 'auto' and
sys.stdout.isatty() and sys.stderr.isatty() and
terminal_supports_color and terminal_supports_color and
os.getenv('TERM') != 'dumb' os.getenv('TERM') != 'dumb'
) )

View file

@ -93,7 +93,7 @@ def _original_lines(
retry: bool = False, retry: bool = False,
) -> Tuple[List[str], List[int]]: ) -> Tuple[List[str], List[int]]:
"""detect `rev:` lines or reformat the file""" """detect `rev:` lines or reformat the file"""
with open(path) as f: with open(path, newline='') as f:
original = f.read() original = f.read()
lines = original.splitlines(True) lines = original.splitlines(True)
@ -126,7 +126,7 @@ def _write_new_config(path: str, rev_infos: List[Optional[RevInfo]]) -> None:
comment = match[4] comment = match[4]
lines[idx] = f'{match[1]}rev:{match[2]}{new_rev}{comment}{match[5]}' lines[idx] = f'{match[1]}rev:{match[2]}{new_rev}{comment}{match[5]}'
with open(path, 'w') as f: with open(path, 'w', newline='') as f:
f.write(''.join(lines)) f.write(''.join(lines))

View file

@ -147,15 +147,44 @@ def _pre_push_ns(
return None return None
_EXPECTED_ARG_LENGTH_BY_HOOK = {
'commit-msg': 1,
'post-checkout': 3,
'pre-commit': 0,
'pre-merge-commit': 0,
'pre-push': 2,
}
def _check_args_length(hook_type: str, args: Sequence[str]) -> None:
if hook_type == 'prepare-commit-msg':
if len(args) < 1 or len(args) > 3:
raise SystemExit(
f'hook-impl for {hook_type} expected 1, 2, or 3 arguments '
f'but got {len(args)}: {args}',
)
elif hook_type in _EXPECTED_ARG_LENGTH_BY_HOOK:
expected = _EXPECTED_ARG_LENGTH_BY_HOOK[hook_type]
if len(args) != expected:
arguments_s = 'argument' if expected == 1 else 'arguments'
raise SystemExit(
f'hook-impl for {hook_type} expected {expected} {arguments_s} '
f'but got {len(args)}: {args}',
)
else:
raise AssertionError(f'unexpected hook type: {hook_type}')
def _run_ns( def _run_ns(
hook_type: str, hook_type: str,
color: bool, color: bool,
args: Sequence[str], args: Sequence[str],
stdin: bytes, stdin: bytes,
) -> Optional[argparse.Namespace]: ) -> Optional[argparse.Namespace]:
_check_args_length(hook_type, args)
if hook_type == 'pre-push': if hook_type == 'pre-push':
return _pre_push_ns(color, args, stdin) return _pre_push_ns(color, args, stdin)
elif hook_type in {'prepare-commit-msg', 'commit-msg'}: elif hook_type in {'commit-msg', 'prepare-commit-msg'}:
return _ns(hook_type, color, commit_msg_filename=args[0]) return _ns(hook_type, color, commit_msg_filename=args[0])
elif hook_type in {'pre-merge-commit', 'pre-commit'}: elif hook_type in {'pre-merge-commit', 'pre-commit'}:
return _ns(hook_type, color) return _ns(hook_type, color)

View file

@ -6,6 +6,7 @@ import os
import re import re
import subprocess import subprocess
import time import time
import unicodedata
from typing import Any from typing import Any
from typing import Collection from typing import Collection
from typing import Dict from typing import Dict
@ -33,8 +34,13 @@ from pre_commit.util import EnvironT
logger = logging.getLogger('pre_commit') logger = logging.getLogger('pre_commit')
def _len_cjk(msg: str) -> int:
widths = {'A': 1, 'F': 2, 'H': 1, 'N': 1, 'Na': 1, 'W': 2}
return sum(widths[unicodedata.east_asian_width(c)] for c in msg)
def _start_msg(*, start: str, cols: int, end_len: int) -> str: def _start_msg(*, start: str, cols: int, end_len: int) -> str:
dots = '.' * (cols - len(start) - end_len - 1) dots = '.' * (cols - _len_cjk(start) - end_len - 1)
return f'{start}{dots}' return f'{start}{dots}'
@ -47,7 +53,7 @@ def _full_msg(
use_color: bool, use_color: bool,
postfix: str = '', postfix: str = '',
) -> str: ) -> str:
dots = '.' * (cols - len(start) - len(postfix) - len(end_msg) - 1) dots = '.' * (cols - _len_cjk(start) - len(postfix) - len(end_msg) - 1)
end = color.format_color(end_msg, end_color, use_color) end = color.format_color(end_msg, end_color, use_color)
return f'{start}{dots}{postfix}{end}\n' return f'{start}{dots}{postfix}{end}\n'
@ -206,7 +212,7 @@ def _compute_cols(hooks: Sequence[Hook]) -> int:
Hook name...(no files to check) Skipped Hook name...(no files to check) Skipped
""" """
if hooks: if hooks:
name_len = max(len(hook.name) for hook in hooks) name_len = max(_len_cjk(hook.name) for hook in hooks)
else: else:
name_len = 0 name_len = 0

View file

@ -76,18 +76,18 @@ def install_environment(
os.mkdir(directory) os.mkdir(directory)
def get_docker_user() -> str: # pragma: win32 no cover def get_docker_user() -> Tuple[str, ...]: # pragma: win32 no cover
try: try:
return f'{os.getuid()}:{os.getgid()}' return ('-u', f'{os.getuid()}:{os.getgid()}')
except AttributeError: except AttributeError:
return '1000:1000' return ()
def docker_cmd() -> Tuple[str, ...]: # pragma: win32 no cover def docker_cmd() -> Tuple[str, ...]: # pragma: win32 no cover
return ( return (
'docker', 'run', 'docker', 'run',
'--rm', '--rm',
'-u', get_docker_user(), *get_docker_user(),
# https://docs.docker.com/engine/reference/commandline/run/#mount-volumes-from-container-volumes-from # 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 # The `Z` option tells Docker to label the content with a private
# unshared label. Only the current container can use a private volume. # unshared label. Only the current container can use a private volume.

View file

@ -1,4 +1,5 @@
import contextlib import contextlib
import functools
import os import os
import sys import sys
from typing import Generator from typing import Generator
@ -6,6 +7,7 @@ from typing import Sequence
from typing import Tuple from typing import Tuple
import pre_commit.constants as C import pre_commit.constants as C
from pre_commit import parse_shebang
from pre_commit.envcontext import envcontext from pre_commit.envcontext import envcontext
from pre_commit.envcontext import PatchesT from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import Var from pre_commit.envcontext import Var
@ -18,10 +20,22 @@ from pre_commit.util import cmd_output
from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_b
ENVIRONMENT_DIR = 'node_env' ENVIRONMENT_DIR = 'node_env'
get_default_version = helpers.basic_get_default_version
healthy = helpers.basic_healthy healthy = helpers.basic_healthy
@functools.lru_cache(maxsize=1)
def get_default_version() -> str:
# nodeenv does not yet support `-n system` on windows
if sys.platform == 'win32':
return C.DEFAULT
# if node is already installed, we can save a bunch of setup time by
# using the installed version
elif all(parse_shebang.find_executable(exe) for exe in ('node', 'npm')):
return 'system'
else:
return C.DEFAULT
def _envdir(prefix: Prefix, version: str) -> str: def _envdir(prefix: Prefix, version: str) -> str:
directory = helpers.environment_dir(ENVIRONMENT_DIR, version) directory = helpers.environment_dir(ENVIRONMENT_DIR, version)
return prefix.path(directory) return prefix.path(directory)

View file

@ -182,8 +182,8 @@ def py_interface(
version: str, version: str,
additional_dependencies: Sequence[str], additional_dependencies: Sequence[str],
) -> None: ) -> None:
additional_dependencies = tuple(additional_dependencies)
directory = helpers.environment_dir(_dir, version) directory = helpers.environment_dir(_dir, version)
install = ('python', '-mpip', 'install', '.', *additional_dependencies)
env_dir = prefix.path(directory) env_dir = prefix.path(directory)
with clean_path_on_failure(env_dir): with clean_path_on_failure(env_dir):
@ -193,9 +193,7 @@ def py_interface(
python = os.path.realpath(sys.executable) python = os.path.realpath(sys.executable)
_make_venv(env_dir, python) _make_venv(env_dir, python)
with in_env(prefix, version): with in_env(prefix, version):
helpers.run_setup_cmd( helpers.run_setup_cmd(prefix, install)
prefix, ('pip', 'install', '.') + additional_dependencies,
)
return in_env, healthy, run_hook, install_environment return in_env, healthy, run_hook, install_environment

View file

@ -1,6 +1,6 @@
[metadata] [metadata]
name = pre_commit name = pre_commit
version = 2.2.0 version = 2.3.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

@ -29,26 +29,26 @@ def test_use_color_always():
def test_use_color_no_tty(): def test_use_color_no_tty():
with mock.patch.object(sys.stdout, 'isatty', return_value=False): with mock.patch.object(sys.stderr, 'isatty', return_value=False):
assert use_color('auto') is False assert use_color('auto') is False
def test_use_color_tty_with_color_support(): def test_use_color_tty_with_color_support():
with mock.patch.object(sys.stdout, 'isatty', return_value=True): with mock.patch.object(sys.stderr, 'isatty', return_value=True):
with mock.patch('pre_commit.color.terminal_supports_color', True): with mock.patch('pre_commit.color.terminal_supports_color', True):
with envcontext.envcontext((('TERM', envcontext.UNSET),)): with envcontext.envcontext((('TERM', envcontext.UNSET),)):
assert use_color('auto') is True assert use_color('auto') is True
def test_use_color_tty_without_color_support(): def test_use_color_tty_without_color_support():
with mock.patch.object(sys.stdout, 'isatty', return_value=True): with mock.patch.object(sys.stderr, 'isatty', return_value=True):
with mock.patch('pre_commit.color.terminal_supports_color', False): with mock.patch('pre_commit.color.terminal_supports_color', False):
with envcontext.envcontext((('TERM', envcontext.UNSET),)): with envcontext.envcontext((('TERM', envcontext.UNSET),)):
assert use_color('auto') is False assert use_color('auto') is False
def test_use_color_dumb_term(): def test_use_color_dumb_term():
with mock.patch.object(sys.stdout, 'isatty', return_value=True): with mock.patch.object(sys.stderr, 'isatty', return_value=True):
with mock.patch('pre_commit.color.terminal_supports_color', True): with mock.patch('pre_commit.color.terminal_supports_color', True):
with envcontext.envcontext((('TERM', 'dumb'),)): with envcontext.envcontext((('TERM', 'dumb'),)):
assert use_color('auto') is False assert use_color('auto') is False

View file

@ -263,6 +263,45 @@ def test_does_not_reformat(tmpdir, out_of_date, store):
assert cfg.read() == expected assert cfg.read() == expected
def test_does_not_change_mixed_endlines_read(up_to_date, tmpdir, store):
fmt = (
'repos:\n'
'- repo: {}\n'
' rev: {} # definitely the version I want!\r\n'
' hooks:\r\n'
' - id: foo\n'
' # These args are because reasons!\r\n'
' args: [foo, bar, baz]\r\n'
)
cfg = tmpdir.join(C.CONFIG_FILE)
expected = fmt.format(up_to_date, git.head_rev(up_to_date)).encode()
cfg.write_binary(expected)
assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0
assert cfg.read_binary() == expected
def test_does_not_change_mixed_endlines_write(tmpdir, out_of_date, store):
fmt = (
'repos:\n'
'- repo: {}\n'
' rev: {} # definitely the version I want!\r\n'
' hooks:\r\n'
' - id: foo\n'
' # These args are because reasons!\r\n'
' args: [foo, bar, baz]\r\n'
)
cfg = tmpdir.join(C.CONFIG_FILE)
cfg.write_binary(
fmt.format(out_of_date.path, out_of_date.original_rev).encode(),
)
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, store, tmpdir): def test_loses_formatting_when_not_detectable(out_of_date, store, tmpdir):
"""A best-effort attempt is made at updating rev without rewriting """A best-effort attempt is made at updating rev without rewriting
formatting. When the original formatting cannot be detected, this formatting. When the original formatting cannot be detected, this

View file

@ -89,6 +89,51 @@ def test_run_legacy_recursive(tmpdir):
call() call()
@pytest.mark.parametrize(
('hook_type', 'args'),
(
('pre-commit', []),
('pre-merge-commit', []),
('pre-push', ['branch_name', 'remote_name']),
('commit-msg', ['.git/COMMIT_EDITMSG']),
('post-checkout', ['old_head', 'new_head', '1']),
# multiple choices for commit-editmsg
('prepare-commit-msg', ['.git/COMMIT_EDITMSG']),
('prepare-commit-msg', ['.git/COMMIT_EDITMSG', 'message']),
('prepare-commit-msg', ['.git/COMMIT_EDITMSG', 'commit', 'deadbeef']),
),
)
def test_check_args_length_ok(hook_type, args):
hook_impl._check_args_length(hook_type, args)
def test_check_args_length_error_too_many_plural():
with pytest.raises(SystemExit) as excinfo:
hook_impl._check_args_length('pre-commit', ['run', '--all-files'])
msg, = excinfo.value.args
assert msg == (
'hook-impl for pre-commit expected 0 arguments but got 2: '
"['run', '--all-files']"
)
def test_check_args_length_error_too_many_singluar():
with pytest.raises(SystemExit) as excinfo:
hook_impl._check_args_length('commit-msg', [])
msg, = excinfo.value.args
assert msg == 'hook-impl for commit-msg expected 1 argument but got 0: []'
def test_check_args_length_prepare_commit_msg_error():
with pytest.raises(SystemExit) as excinfo:
hook_impl._check_args_length('prepare-commit-msg', [])
msg, = excinfo.value.args
assert msg == (
'hook-impl for prepare-commit-msg expected 1, 2, or 3 arguments '
'but got 0: []'
)
def test_run_ns_pre_commit(): def test_run_ns_pre_commit():
ns = hook_impl._run_ns('pre-commit', True, (), b'') ns = hook_impl._run_ns('pre-commit', True, (), b'')
assert ns is not None assert ns is not None

View file

@ -52,6 +52,18 @@ def test_full_msg():
assert ret == 'start......end\n' assert ret == 'start......end\n'
def test_full_msg_with_cjk():
ret = _full_msg(
start='啊あ아',
end_msg='end',
end_color='',
use_color=False,
cols=15,
)
# 5 dots: 15 - 6 - 3 - 1
assert ret == '啊あ아.....end\n'
def test_full_msg_with_color(): def test_full_msg_with_color():
ret = _full_msg( ret = _full_msg(
start='start', start='start',

View file

@ -20,4 +20,4 @@ def test_docker_fallback_user():
getuid=invalid_attribute, getuid=invalid_attribute,
getgid=invalid_attribute, getgid=invalid_attribute,
): ):
assert docker.get_docker_user() == '1000:1000' assert docker.get_docker_user() == ()

View file

@ -0,0 +1,47 @@
import sys
from unittest import mock
import pytest
import pre_commit.constants as C
from pre_commit import parse_shebang
from pre_commit.languages.node import get_default_version
ACTUAL_GET_DEFAULT_VERSION = get_default_version.__wrapped__
@pytest.fixture
def is_linux():
with mock.patch.object(sys, 'platform', 'linux'):
yield
@pytest.fixture
def is_win32():
with mock.patch.object(sys, 'platform', 'win32'):
yield
@pytest.fixture
def find_exe_mck():
with mock.patch.object(parse_shebang, 'find_executable') as mck:
yield mck
@pytest.mark.usefixtures('is_linux')
def test_sets_system_when_node_and_npm_are_available(find_exe_mck):
find_exe_mck.return_value = '/path/to/exe'
assert ACTUAL_GET_DEFAULT_VERSION() == 'system'
@pytest.mark.usefixtures('is_linux')
def test_uses_default_when_node_and_npm_are_not_available(find_exe_mck):
find_exe_mck.return_value = None
assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT
@pytest.mark.usefixtures('is_win32')
def test_sets_default_on_windows(find_exe_mck):
find_exe_mck.return_value = '/path/to/exe'
assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT

View file

@ -131,9 +131,9 @@ def test_python_hook(tempdir_factory, store):
def test_python_hook_default_version(tempdir_factory, store): def test_python_hook_default_version(tempdir_factory, store):
# make sure that this continues to work for platforms where default # make sure that this continues to work for platforms where default
# language detection does not work # language detection does not work
with mock.patch.object( returns_default = mock.Mock(return_value=C.DEFAULT)
python, 'get_default_version', return_value=C.DEFAULT, lang = languages['python']._replace(get_default_version=returns_default)
): with mock.patch.dict(languages, python=lang):
test_python_hook(tempdir_factory, store) test_python_hook(tempdir_factory, store)
@ -243,6 +243,15 @@ def test_run_a_node_hook(tempdir_factory, store):
) )
def test_run_a_node_hook_default_version(tempdir_factory, store):
# make sure that this continues to work for platforms where node is not
# installed at the system
returns_default = mock.Mock(return_value=C.DEFAULT)
lang = languages['node']._replace(get_default_version=returns_default)
with mock.patch.dict(languages, node=lang):
test_run_a_node_hook(tempdir_factory, store)
def test_run_versioned_node_hook(tempdir_factory, store): def test_run_versioned_node_hook(tempdir_factory, store):
_test_hook_repo( _test_hook_repo(
tempdir_factory, store, 'node_versioned_hooks_repo', tempdir_factory, store, 'node_versioned_hooks_repo',