diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8804d9d2..0440edfb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 + rev: v4.3.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -14,17 +14,17 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder_python_imports - rev: v3.0.1 + rev: v3.1.0 hooks: - id: reorder-python-imports args: [--py37-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma - rev: v2.2.2 + rev: v2.2.3 hooks: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.31.1 + rev: v2.32.1 hooks: - id: pyupgrade args: [--py37-plus] @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.942 + rev: v0.960 hooks: - id: mypy additional_dependencies: [types-all] diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 9e835d58..1a6056bd 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -3,6 +3,7 @@ description: prevents giant files from being committed. entry: check-added-large-files language: python + stages: [commit, push, manual] - id: check-ast name: check python ast description: simply checks whether the files parse as valid python. @@ -163,7 +164,7 @@ types: [text] - id: name-tests-test name: python tests naming - description: this verifies that test files are named correctly. + description: verifies that test files are named correctly. entry: name-tests-test language: python files: (^|/)tests/.+\.py$ diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a2ae68b..d6e3171d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +4.3.0 - 2022-06-07 +================== + +### Features +- `check-executables-have-shebangs`: use `git config core.fileMode` to + determine if it should query `git`. + - #730 PR by @Kurt-von-Laven. +- `name-tests-test`: add `--pytest-test-first` test convention. + - #779 PR by @asottile. + +### Fixes +- `check-shebang-scripts-are-executable`: update windows instructions. + - #774 PR by @mdeweerd. + - #770 issue by @mdeweerd. +- `check-toml`: use stdlib `tomllib` when available. + - #771 PR by @DanielNoord. + - #755 issue by @sognetic. +- `check-added-large-files`: don't run on non-file `stages`. + - #778 PR by @asottile. + - #777 issue by @skyj. + 4.2.0 - 2022-04-06 ================== diff --git a/README.md b/README.md index 73688319..411529a0 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Add this to your `.pre-commit-config.yaml` ```yaml - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 # Use the ref you want to point at + rev: v4.3.0 # Use the ref you want to point at hooks: - id: trailing-whitespace # - id: ... @@ -57,6 +57,7 @@ Attempts to load all json files to verify syntax. #### `check-merge-conflict` Check for files that contain merge conflict strings. + - `--assume-in-merge` - Allows running the hook when there is no ongoing merge operation #### `check-shebang-scripts-are-executable` Checks that scripts with shebangs are executable. @@ -141,8 +142,10 @@ Replaces or checks mixed line ending. - `no` - Checks if there is any mixed line ending without modifying any file. #### `name-tests-test` -Assert that files in tests/ end in `_test.py`. - - Use `args: ['--django']` to match `test*.py` instead. +verifies that test files are named correctly. +- `--pytest` (the default): ensure tests match `.*_test\.py` +- `--pytest-test-first`: ensure tests match `test_.*\.py` +- `--django` / `--unittest`: ensure tests match `test.*\.py` #### `no-commit-to-branch` Protect specific branches from direct checkins. diff --git a/pre_commit_hooks/check_executables_have_shebangs.py b/pre_commit_hooks/check_executables_have_shebangs.py index 6b5402ac..d8e4f490 100644 --- a/pre_commit_hooks/check_executables_have_shebangs.py +++ b/pre_commit_hooks/check_executables_have_shebangs.py @@ -15,7 +15,10 @@ def check_executables(paths: list[str]) -> int: - if sys.platform == 'win32': # pragma: win32 cover + fs_tracks_executable_bit = cmd_output( + 'git', 'config', 'core.fileMode', retcode=None, + ).strip() + if fs_tracks_executable_bit == 'false': # pragma: win32 cover return _check_git_filemode(paths) else: # pragma: win32 no cover retv = 0 diff --git a/pre_commit_hooks/check_shebang_scripts_are_executable.py b/pre_commit_hooks/check_shebang_scripts_are_executable.py index 0f35650b..621696ce 100644 --- a/pre_commit_hooks/check_shebang_scripts_are_executable.py +++ b/pre_commit_hooks/check_shebang_scripts_are_executable.py @@ -34,6 +34,8 @@ def _message(path: str) -> None: f'{path}: has a shebang but is not marked executable!\n' f' If it is supposed to be executable, try: ' f'`chmod +x {shlex.quote(path)}`\n' + f' If on Windows, you may also need to: ' + f'`git add --chmod=+x {shlex.quote(path)}`\n' f' If it not supposed to be executable, double-check its shebang ' f'is wanted.\n', file=sys.stderr, diff --git a/pre_commit_hooks/check_toml.py b/pre_commit_hooks/check_toml.py index 88f70865..0407371e 100644 --- a/pre_commit_hooks/check_toml.py +++ b/pre_commit_hooks/check_toml.py @@ -1,9 +1,13 @@ from __future__ import annotations import argparse +import sys from typing import Sequence -import toml +if sys.version_info >= (3, 11): # pragma: >=3.11 cover + import tomllib +else: # pragma: <3.11 cover + import tomli as tomllib def main(argv: Sequence[str] | None = None) -> int: @@ -14,8 +18,9 @@ def main(argv: Sequence[str] | None = None) -> int: retval = 0 for filename in args.filenames: try: - toml.load(filename) - except toml.TomlDecodeError as exc: + with open(filename, mode='rb') as fp: + tomllib.load(fp) + except tomllib.TOMLDecodeError as exc: print(f'{filename}: {exc}') retval = 1 return retval diff --git a/pre_commit_hooks/tests_should_end_in_test.py b/pre_commit_hooks/tests_should_end_in_test.py index e1ffe367..e7842af7 100644 --- a/pre_commit_hooks/tests_should_end_in_test.py +++ b/pre_commit_hooks/tests_should_end_in_test.py @@ -9,23 +9,42 @@ def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*') - parser.add_argument( - '--django', default=False, action='store_true', - help='Use Django-style test naming pattern (test*.py)', + mutex = parser.add_mutually_exclusive_group() + mutex.add_argument( + '--pytest', + dest='pattern', + action='store_const', + const=r'.*_test\.py', + default=r'.*_test\.py', + help='(the default) ensure tests match %(const)s', + ) + mutex.add_argument( + '--pytest-test-first', + dest='pattern', + action='store_const', + const=r'test_.*\.py', + help='ensure tests match %(const)s', + ) + mutex.add_argument( + '--django', '--unittest', + dest='pattern', + action='store_const', + const=r'test.*\.py', + help='ensure tests match %(const)s', ) args = parser.parse_args(argv) retcode = 0 - test_name_pattern = r'test.*\.py' if args.django else r'.*_test\.py' + reg = re.compile(args.pattern) for filename in args.filenames: base = os.path.basename(filename) if ( - not re.match(test_name_pattern, base) and + not reg.fullmatch(base) and not base == '__init__.py' and not base == 'conftest.py' ): retcode = 1 - print(f'{filename} does not match pattern "{test_name_pattern}"') + print(f'{filename} does not match pattern "{args.pattern}"') return retcode diff --git a/setup.cfg b/setup.cfg index 8247f311..f5015715 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit_hooks -version = 4.2.0 +version = 4.3.0 description = Some out-of-the-box hooks for pre-commit. long_description = file: README.md long_description_content_type = text/markdown @@ -24,7 +24,7 @@ classifiers = packages = find: install_requires = ruamel.yaml>=0.15 - toml + tomli>=1.1.0;python_version<"3.11" python_requires = >=3.7 [options.packages.find] diff --git a/tests/tests_should_end_in_test_test.py b/tests/tests_should_end_in_test_test.py index dc3744b8..2b5a0dea 100644 --- a/tests/tests_should_end_in_test_test.py +++ b/tests/tests_should_end_in_test_test.py @@ -43,3 +43,8 @@ def test_main_not_django_fails(): def test_main_django_fails(): ret = main(['--django', 'foo_test.py', 'test_bar.py', 'test_baz.py']) assert ret == 1 + + +def test_main_pytest_test_first(): + assert main(['--pytest-test-first', 'test_foo.py']) == 0 + assert main(['--pytest-test-first', 'foo_test.py']) == 1